/* * Copyright 2005-2015 by BerryWorks Software, LLC. All rights reserved. * * This file is part of EDIReader. You may obtain a license for its use directly from * BerryWorks Software, and you may also choose to use this software under the terms of the * GPL version 3. Other products in the EDIReader software suite are available only by licensing * with BerryWorks. Only those files bearing the GPL statement below are available under the GPL. * * EDIReader is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * EDIReader is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with EDIReader. If not, * see <http://www.gnu.org/licenses/>. */ package com.berryworks.edireader; import com.berryworks.edireader.error.ErrorMessages; import com.berryworks.edireader.error.MissingMandatoryElementException; import com.berryworks.edireader.tokenizer.Token; import com.berryworks.edireader.util.ContentHandlerBase64Encoder; import com.berryworks.edireader.util.FixedLength; import com.berryworks.edireader.util.sax.QueuedContentHandler; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import java.io.IOException; import static com.berryworks.edireader.tokenizer.Token.TokenType.SEGMENT_END; import static com.berryworks.edireader.tokenizer.Token.TokenType.SEGMENT_START; /** * Reads and parses ANSI X.12 EDI interchanges. This class is not normally * constructed explicitly from outside the package, although it is declared * public for special cases. The recommended use of this class is to first * establish an EDIReader using one of the factory techniques; when the * EDIReader is called upon to parse the EDI data, it determines which EDI * standard applies and internally constructs the proper subclass to continue * with parsing. */ public class AnsiReader extends StandardReader { /** * Group-level function code (for example: PO) */ protected String groupFunctionCode; /** * Group-level application sender */ protected String groupSender; /** * Group-level application receiver */ protected String groupReceiver; /** * Group-level version (for example: 003040) */ protected String groupVersion; /** * Group-level date (for example: 20040410 or 040410) */ protected String groupDate; @Override protected Token recognizeBeginning() throws IOException, EDISyntaxException { Token t = getTokenizer().nextToken(); if ((t.getType() != SEGMENT_START) || (!t.valueEquals("ISA"))) { throw new EDISyntaxException(X12_MISSING_ISA); } return t; } /** * Parse ANSI Interchange (ISA ...) */ @Override protected Token parseInterchange(Token token) throws SAXException, IOException { setGroupCount(0); getInterchangeAttributes().clear(); getInterchangeAttributes().addCDATA(getXMLTags().getStandard(), "ANSI X.12"); String authQual = getFixedLengthISAField(2); String authInfo = getFixedLengthISAField(10); String securityQual = getFixedLengthISAField(2); String securityInfo = getFixedLengthISAField(10); getInterchangeAttributes().addCDATA(getXMLTags().getAuthorizationQual(), authQual); getInterchangeAttributes().addCDATA(getXMLTags().getAuthorization(), authInfo); getInterchangeAttributes().addCDATA(getXMLTags().getSecurityQual(), securityQual); getInterchangeAttributes().addCDATA(getXMLTags().getSecurity(), securityInfo); String fromQual = getFixedLengthISAField(2); process("ISA05", fromQual); String fromId = getFixedLengthISAField(15, false); process("ISA06", fromId); String toQual = getFixedLengthISAField(2); process("ISA07", toQual); String toId = getFixedLengthISAField(15, false); process("ISA08", fromId); getInterchangeAttributes().addCDATA( getXMLTags().getDate(), getTokenizer().nextSimpleValue()); // Control time - relax the check for length of exactly 4 String controlTime = getTokenizer().nextSimpleValue(); getInterchangeAttributes().addCDATA(getXMLTags().getTime(), controlTime); // The standards id, typically "U", through version 4010 // The repetition character, version 4020 and later int separator = getTokenizer().getRepetitionSeparator(); if (separator == -1) { // No repetition char is in effect. It is therefore safe to interpret this next // element as the standardsId used through version 4010 of ANSI X12. String standardsId = getFixedLengthISAField(1); getInterchangeAttributes().addCDATA(getXMLTags().getStandardsId(), standardsId); } else { // A repetition char is in effect, presumably previewed from this ISA segment we // are now parsing. Therefore, we treat this field in accordance with version 4020 // or later where it repetition character instead of a standardsId. // Temporarily disable the repetition char so that we can parse over this element // as normal data. getTokenizer().setRepetitionSeparator(-1); getFixedLengthISAField(1); getTokenizer().setRepetitionSeparator(separator); } String versionId = getFixedLengthISAField(5); getInterchangeAttributes().addCDATA(getXMLTags().getVersion(), versionId); setInterchangeControlNumber(getFixedLengthISAField(9)); getInterchangeAttributes().addCDATA(getXMLTags().getControl(), getInterchangeControlNumber()); // Acknowledgement Request String ackRequest = getTokenizer().nextSimpleValue(); getInterchangeAttributes().addCDATA(getXMLTags().getAcknowledgementRequest(), ackRequest); // Test Indicator String testIndicator = getTokenizer().nextSimpleValue(); getInterchangeAttributes().addCDATA(getXMLTags().getTestIndicator(), testIndicator); // Go ahead and parse tokens until the end of the segment is reached while (getTokenizer().nextToken().getType() != SEGMENT_END) if (getTokenizer().getElementInSegmentCount() > 30) throw new EDISyntaxException(TOO_MANY_ISA_FIELDS, getTokenizer()); // Now make the the callbacks to the ContentHandler startInterchange(getInterchangeAttributes()); getInterchangeAttributes().clear(); startElement(getXMLTags().getSenderTag(), getInterchangeAttributes()); getInterchangeAttributes().addCDATA(getXMLTags().getIdAttribute(), fromId); getInterchangeAttributes().addCDATA(getXMLTags().getQualifierAttribute(), fromQual); startSenderAddress(getInterchangeAttributes()); endElement(getXMLTags().getAddressTag()); endElement(getXMLTags().getSenderTag()); getInterchangeAttributes().clear(); startElement(getXMLTags().getReceiverTag(), getInterchangeAttributes()); getInterchangeAttributes().addCDATA(getXMLTags().getIdAttribute(), toId); getInterchangeAttributes().addCDATA(getXMLTags().getQualifierAttribute(), toQual); startReceiverAddress(getInterchangeAttributes()); endElement(getXMLTags().getAddressTag()); endElement(getXMLTags().getReceiverTag()); label: while (true) { token = getTokenizer().nextToken(); if (token.getType() != SEGMENT_START) throw new EDISyntaxException(INVALID_BEGINNING_OF_SEGMENT, getTokenizer().getSegmentCount()); String sType = token.getValue(); switch (sType) { case "GS": setGroupCount(1 + getGroupCount()); parseFunctionalGroup(token); break; case "TA1": parseTA1(token); break; case "IEA": break label; default: throw new EDISyntaxException(UNEXPECTED_SEGMENT_IN_CONTEXT, "IEA or GS", sType, getTokenizer()); } } checkGroupCount(getGroupCount(), getTokenizer().nextIntValue(), COUNT_IEA); checkInterchangeControlNumber(getInterchangeControlNumber(), getTokenizer().nextSimpleValue(), CONTROL_NUMBER_IEA); getAckGenerator().generateAcknowledgementWrapup(); getAlternateAckGenerator().generateAcknowledgementWrapup(); endInterchange(); return (getTokenizer().skipSegment()); } protected void parseTA1(Token token) throws SAXException, IOException { EDIAttributes attributes = new EDIAttributes(); String acknowledgedControlNumber = getTokenizer().nextSimpleValue(); attributes.addCDATA(getXMLTags().getControl(), acknowledgedControlNumber); String acknowledgedDate = getTokenizer().nextSimpleValue(); attributes.addCDATA(getXMLTags().getDate(), acknowledgedDate); String acknowledgedTime = getTokenizer().nextSimpleValue(); attributes.addCDATA(getXMLTags().getTime(), acknowledgedTime); String code = getTokenizer().nextSimpleValue(); attributes.addCDATA(getXMLTags().getAcknowledgementCode(), code); String note = getTokenizer().nextSimpleValue(false, true); if (note != null) { if (note.length() > 0) { attributes.addCDATA(getXMLTags().getNotCode(), note); } getTokenizer().skipSegment(); } startElement(getXMLTags().getAcknowledgementTag(), attributes); endElement(getXMLTags().getAcknowledgementTag()); } protected String getFixedLengthISAField(int expectedLength) throws IOException, SAXException { return getFixedLengthISAField(expectedLength, true); } protected String getFixedLengthISAField(int expectedLength, boolean enforce) throws SAXException, IOException { String field = getTokenizer().nextSimpleValue(); if (field.length() != expectedLength) { if (enforce) throw new EDISyntaxException(ISA_FIELD_WIDTH, expectedLength, field .length(), getTokenizer()); else field = FixedLength.valueOf(field, expectedLength); } return field; } /** * Parse ANSI Functional Group (GS .. GE) * * @param token parsed token that caused this method to be called * @return token most recently parsed by this method * @throws SAXException for problem emitting SAX events * @throws IOException for problem reading EDI data */ protected Token parseFunctionalGroup(Token token) throws SAXException, IOException { int docCount = 0; getGroupAttributes().clear(); getGroupAttributes().addCDATA(getXMLTags().getGroupType(), groupFunctionCode = getTokenizer().nextSimpleValue()); groupSender = getTokenizer().nextSimpleValue(false); process("GS02", groupSender); groupReceiver = getTokenizer().nextSimpleValue(false); process("GS03", groupReceiver); groupDate = getTokenizer().nextSimpleValue(); getGroupAttributes().addCDATA(getXMLTags().getApplSender(), groupSender); getGroupAttributes().addCDATA(getXMLTags().getApplReceiver(), groupReceiver); getGroupAttributes().addCDATA(getXMLTags().getDate(), groupDate); String value = ""; try { value = getTokenizer().nextSimpleValue(); } catch (EDISyntaxException e) { if (e.getMessage().startsWith("Mandatory")) { MissingMandatoryElementException missingMandatoryElementException = new MissingMandatoryElementException(MANDATORY_ELEMENT_MISSING, "at least one non-space character", "(empty)", getTokenizer()); setSyntaxException(missingMandatoryElementException); if (!recover(missingMandatoryElementException)) throw missingMandatoryElementException; } else throw e; } getGroupAttributes().addCDATA(getXMLTags().getTime(), value); setGroupControlNumber(getTokenizer().nextSimpleValue()); getGroupAttributes().addCDATA(getXMLTags().getControl(), getGroupControlNumber()); getGroupAttributes().addCDATA(getXMLTags().getStandardCode(), getTokenizer().nextSimpleValue()); // Handle the groupVersion at the end of the segment. This is a bit tricky since // the groupVersion may be omitted causing us to encounter the end of segment earlier // than expected. Token t = getTokenizer().nextToken(); if (t.getType() == SEGMENT_END) { groupVersion = ""; } else { groupVersion = t.getValue(); getGroupAttributes().addCDATA(getXMLTags().getStandardVersion(), groupVersion); getTokenizer().skipSegment(); } startElement(getXMLTags().getGroupTag(), getGroupAttributes()); getAckGenerator().generateAcknowledgmentHeader(getFirstSegment(), groupSender, groupReceiver, groupDate.length(), groupVersion, groupFunctionCode, getGroupControlNumber()); getAlternateAckGenerator().generateAcknowledgmentHeader(getFirstSegment(), groupSender, groupReceiver, groupDate.length(), groupVersion, groupFunctionCode, getGroupControlNumber()); label: while (true) { token = getTokenizer().nextToken(); if (token.getType() != SEGMENT_START) throw new EDISyntaxException(INVALID_BEGINNING_OF_SEGMENT, getTokenizer().getSegmentCount()); String sType = token.getValue(); switch (sType) { case "ST": docCount++; parseDocument(token); break; case "GE": break label; default: throw new EDISyntaxException(UNEXPECTED_SEGMENT_IN_CONTEXT, "GE or ST", sType, getTokenizer()); } } checkTransactionCount(docCount, getTokenizer().nextIntValue(), COUNT_GE); checkGroupControlNumber(getGroupControlNumber(), getTokenizer().nextSimpleValue(), CONTROL_NUMBER_GE); endElement(getXMLTags().getGroupTag()); getAckGenerator().generateGroupAcknowledgmentTrailer(docCount); getAlternateAckGenerator().generateGroupAcknowledgmentTrailer(docCount); return (getTokenizer().skipSegment()); } protected void process(String ediElement, String value) throws SAXException { } /** * Parse ANSI Document/Transaction Set (ST .. SE) * * @param token parsed token that caused this method to be called * @return token most recently parsed by this method * @throws SAXException for problem emitting SAX events * @throws IOException for problem reading EDI data */ protected Token parseDocument(Token token) throws SAXException, IOException { String control; String documentType; Token t; int segCount = 2; if (getTransactionCallback() != null) getTransactionCallback().startTransaction(token.getValue()); getDocumentAttributes().clear(); getDocumentAttributes().addCDATA(getXMLTags().getDocumentType(), documentType = getTokenizer().nextSimpleValue()); PluginController.setDebug(debug); String version = groupVersion; if (version.length() > 6) version = version.substring(0, 6); String code = getGroupAttributes().getValue(getXMLTags().getStandardCode()); PluginController pluginController = getPluginControllerFactory().create("ANSI", documentType, code, version, getTokenizer()); boolean wrapped = wrapContentHandlerIfNeeded(pluginController); if (pluginController.isEnabled()) getDocumentAttributes().addCDATA(getXMLTags().getName(), pluginController.getDocumentName()); getDocumentAttributes().addCDATA(getXMLTags().getControl(), control = getTokenizer().nextSimpleValue()); Token st03Token = getTokenizer().nextToken(); switch (st03Token.getType()) { case SEGMENT_END: break; case SIMPLE: getDocumentAttributes().addCDATA(getXMLTags().getMessageVersion(), st03Token.getValue()); default: getTokenizer().skipSegment(); } startMessage(getDocumentAttributes()); String segmentType; while (!(segmentType = getTokenizer().nextSegment()).equals("SE")) { if (isEnvelopeSegment(segmentType)) throw new EDISyntaxException(ErrorMessages.SE_MISSING, getTokenizer()); segCount++; if ("BIN".equals(segmentType)) { parseBINSequence(); } else { parseSegment(pluginController, segmentType); } } if (wrapped) unwrapContentHandler(pluginController); int toClose = pluginController.getNestingLevel(); for (; toClose > 0; toClose--) endElement(getXMLTags().getLoopTag()); checkSegmentCount(segCount, getTokenizer().nextIntValue(), COUNT_SE); checkTransactionControlNumber(control, getTokenizer().nextSimpleValue(), CONTROL_NUMBER_SE); getAckGenerator().generateTransactionAcknowledgment(documentType, control); getAlternateAckGenerator().generateTransactionAcknowledgment(documentType, control); endElement(getXMLTags().getDocumentTag()); // Skip over this SE segment // return the SEGMENT_END token t = getTokenizer().skipSegment(); if (getTransactionCallback() != null) getTransactionCallback().endTransaction(); return t; } private boolean wrapContentHandlerIfNeeded(PluginController pluginController) { boolean result = false; ContentHandler contentHandler = getContentHandler(); if (contentHandler instanceof QueuedContentHandler) { // If it is already queued, then no need to wrap it } else { if (pluginController.isQueuedContentHandlerRequired()) { setContentHandler(new QueuedContentHandler(contentHandler, 10, getTokenizer())); result = true; } } return result; } private void unwrapContentHandler(PluginController pluginController) { ContentHandler contentHandler = getContentHandler(); if (contentHandler instanceof QueuedContentHandler) { QueuedContentHandler queuedContentHandler = (QueuedContentHandler) contentHandler; try { queuedContentHandler.drainQueue(); } catch (SAXException ignore) { } setContentHandler(queuedContentHandler.getWrappedContentHandler()); } } private boolean isEnvelopeSegment(String segmentType) { return "ISA".equals(segmentType) || "GS".equals(segmentType) || "ST".equals(segmentType) || "SE".equals(segmentType) || "GE".equals(segmentType) || "IEA".equals(segmentType) || "TA1".equals(segmentType); } protected void parseBINSequence() throws SAXException, IOException { String lengthField = ""; int length; try { lengthField = parseStringFromNextElement(); length = Integer.parseInt(lengthField); } catch (EDISyntaxException e) { throw new EDISyntaxException(ErrorMessages.MISSING_BIN_LENGTH, getTokenizer()); } catch (NumberFormatException e) { throw new EDISyntaxException("BIN object length must be numeric instead of " + lengthField, getTokenizer()); } char[] dataObject = getTokenizer().getChars(length); getTokenizer().nextToken(); getDocumentAttributes().clear(); startElement(getXMLTags().getPackageTag(), getDocumentAttributes()); new ContentHandlerBase64Encoder().encode(dataObject, getContentHandler()); endElement(getXMLTags().getPackageTag()); } /** * Preview the ANSI X.12 input before attempting to tokenize it in order to * discover syntactic details including segment terminator and field * delimiter. Upon return, the input stream has been re-positioned so that * the tokenizer can read from the beginning of the interchange. * * @throws EDISyntaxException if invalid EDI is detected * @throws IOException for problem reading EDI data */ @Override public void preview() throws EDISyntaxException, IOException { if (isPreviewed()) throw new EDISyntaxException(INTERNAL_ERROR_MULTIPLE_EOFS); // No release character is supported for ANSI X.12 setRelease(-1); char[] buf = getTokenizer().lookahead(128); if ((buf == null) || (buf.length < 128)) throw new EDISyntaxException(INCOMPLETE_X12); if (!(buf[0] == 'I' && buf[1] == 'S' && buf[2] == 'A')) throw new EDISyntaxException(X12_MISSING_ISA); // ISA* // ...^ (offset 3) char c = buf[3]; setDelimiter(c); int indexOf16thFieldSeparator = indexOf(c, buf, 16); if (indexOf16thFieldSeparator < 0) throw new EDISyntaxException(ISA_SEGMENT_HAS_TOO_FEW_FIELDS); c = buf[indexOf16thFieldSeparator + 1]; if (isAcceptable(c)) setSubDelimiter(c); c = buf[indexOf16thFieldSeparator + 2]; if (isAcceptable(c)) setTerminator(c); else throw new EDISyntaxException(INVALID_SEGMENT_TERMINATOR); setTerminatorSuffix(findTerminatorSuffix(buf, indexOf16thFieldSeparator + 3, 128)); // Determine the repetition character. This changed in 4.6.5 to support repetition chars // introduced in version 4020 of the ANSI X12 standard. int indexOf11thFieldSeparator = indexOf(getDelimiter(), buf, 11); char repetitionChar = buf[indexOf11thFieldSeparator + 1]; if (Character.isLetterOrDigit(repetitionChar) || repetitionChar == getTerminator() || repetitionChar == getDelimiter() || Character.isWhitespace(repetitionChar)) { // This is not a suitable repetition character. // It may be desirable to further check to see if the version number // in the next ISA element indicates 4020 or later; if not, then // repetition characters were not used. setRepetitionSeparator('\000'); } else { setRepetitionSeparator(repetitionChar); } setFirstSegment(new String(buf, 0, indexOf16thFieldSeparator + 3)); setPreviewed(true); } private static int indexOf(char c, char[] buf, int n) { int result = -1; int count = 0; for (int i = 0; i < buf.length; i++) { if (buf[i] == c) { count++; if (count == n) { result = i; break; } } } return result; } protected boolean isAcceptable(char c) { return getDelimiter() != c; } protected static String findTerminatorSuffix(char[] buf, int i, int j) { StringBuilder result = new StringBuilder(); for (int n = i; n < j && !Character.isLetter(buf[n]); n++) result.append(buf[n]); return result.toString(); } @Override public ReplyGenerator getAckGenerator() { if (super.getAckGenerator() == null) setAckGenerator(new AnsiFAGenerator(this, getAckStream())); return super.getAckGenerator(); } @Override public ReplyGenerator getAlternateAckGenerator() { if (super.getAlternateAckGenerator() == null) setAlternateAckGenerator(new Ansi999Generator(this, getAlternateAckStream())); return super.getAlternateAckGenerator(); } }