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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ There may be errors in the structure that are severe enough that they prevent pr
List<String> 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:
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/imsweb/x12/mapping/CustomX12Mapping.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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;
}
}
47 changes: 47 additions & 0 deletions src/main/java/com/imsweb/x12/mapping/X12Mapping.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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);
}
}
107 changes: 78 additions & 29 deletions src/main/java/com/imsweb/x12/reader/X12Reader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -63,12 +59,12 @@ public class X12Reader {
private final Map<String, List<Set<String>>> _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"),
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}
Expand Down
43 changes: 43 additions & 0 deletions src/test/java/com/imsweb/x12/reader/X12ReaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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<Loop> 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");
Expand Down
Loading