View Javadoc
1   /**
2    * The contents of this file are subject to the Mozilla Public License Version 1.1
3    * (the "License"); you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at http://www.mozilla.org/MPL/
5    * Software distributed under the License is distributed on an "AS IS" basis,
6    * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
7    * specific language governing rights and limitations under the License.
8    *
9    * The Original Code is "PipeParser.java".  Description:
10   * "An implementation of Parser that supports traditionally encoded (i.e"
11   *
12   * The Initial Developer of the Original Code is University Health Network. Copyright (C)
13   * 2001.  All Rights Reserved.
14   *
15   * Contributor(s): Kenneth Beaton.
16   *
17   * Alternatively, the contents of this file may be used under the terms of the
18   * GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
19   * applicable instead of those above.  If you wish to allow use of your version of this
20   * file only under the terms of the GPL and not to allow others to use your version
21   * of this file under the MPL, indicate your decision by deleting  the provisions above
22   * and replace  them with the notice and other provisions required by the GPL License.
23   * If you do not delete the provisions above, a recipient may use your version of
24   * this file under either the MPL or the GPL.
25   *
26   */
27  
28  package ca.uhn.hl7v2.parser;
29  
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.StringTokenizer;
33  
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import ca.uhn.hl7v2.ErrorCode;
38  import ca.uhn.hl7v2.HL7Exception;
39  import ca.uhn.hl7v2.Version;
40  import ca.uhn.hl7v2.model.Group;
41  import ca.uhn.hl7v2.model.Message;
42  import ca.uhn.hl7v2.model.Primitive;
43  import ca.uhn.hl7v2.model.Segment;
44  import ca.uhn.hl7v2.model.Structure;
45  import ca.uhn.hl7v2.model.Type;
46  import ca.uhn.hl7v2.util.FilterIterator;
47  import ca.uhn.hl7v2.util.MessageIterator;
48  import ca.uhn.hl7v2.util.Terser;
49  
50  /**
51   * This is a legacy implementation of the PipeParser and should not be used
52   * for new projects.
53   *
54   * In version 1.0 of HAPI, a behaviour was corrected where unexpected segments
55   * would be placed at the tail end of the first segment group encountered. Any
56   * legacy code which still depends on previous behaviour can use this
57   * implementation.
58   *
59   * @author Bryan Tripp (bryan_tripp@sourceforge.net)
60   * @deprecated
61   */
62  class OldPipeParser extends Parser {
63      
64      private static final Logger log = LoggerFactory.getLogger(OldPipeParser.class);
65      
66      private final static String segDelim = "\r"; //see section 2.8 of spec
67      
68      /** Creates a new PipeParser */
69      public OldPipeParser() {
70      }
71  
72      /** 
73       * Creates a new PipeParser 
74       *  
75       * @param theFactory custom factory to use for model class lookup 
76       */
77      public OldPipeParser(ModelClassFactory theFactory) {
78      	super(theFactory);
79      }
80      
81      /**
82       * Returns a String representing the encoding of the given message, if
83       * the encoding is recognized.  For example if the given message appears
84       * to be encoded using HL7 2.x XML rules then "XML" would be returned.
85       * If the encoding is not recognized then null is returned.  That this
86       * method returns a specific encoding does not guarantee that the
87       * message is correctly encoded (e.g. well formed XML) - just that
88       * it is not encoded using any other encoding than the one returned.
89       */
90      public String getEncoding(String message) {
91          String encoding = null;
92          
93          //quit if the string is too short
94          if (message.length() < 4)
95              return null;
96          
97          //see if it looks like this message is | encoded ...
98          boolean ok = true;
99          
100         //string should start with "MSH"
101         if (!message.startsWith("MSH"))
102             return null;
103         
104         //4th character of each segment should be field delimiter
105         char fourthChar = message.charAt(3);
106         StringTokenizer st = new StringTokenizer(message, segDelim, false);
107         while (st.hasMoreTokens()) {
108             String x = st.nextToken();
109             if (x.length() > 0) {
110                 if (Character.isWhitespace(x.charAt(0)))
111                     x = stripLeadingWhitespace(x);
112                 if (x.length() >= 4 && x.charAt(3) != fourthChar)
113                     return null;
114             }
115         }
116         
117         //should be at least 11 field delimiters (because MSH-12 is required)
118         int nextFieldDelimLoc = 0;
119         for (int i = 0; i < 11; i++) {
120             nextFieldDelimLoc = message.indexOf(fourthChar, nextFieldDelimLoc + 1);
121             if (nextFieldDelimLoc < 0)
122                 return null;
123         }
124 
125         encoding = "VB";
126         
127         return encoding;
128     }
129     
130     /**
131      * @return the preferred encoding of this Parser
132      */
133     public String getDefaultEncoding() {
134         return "VB";
135     }
136     
137     /**
138      * Returns true if and only if the given encoding is supported
139      * by this Parser.
140      */
141     public boolean supportsEncoding(String encoding) {
142         boolean supports = false;
143         if (encoding != null && encoding.equals("VB"))
144             supports = true;
145         return supports;
146     }
147     
148     /**
149      * @deprecated this method should not be public 
150      * @param message
151      * @return
152      * @throws HL7Exception
153      * @throws EncodingNotSupportedException
154      */
155     public String getMessageStructure(String message) throws HL7Exception, EncodingNotSupportedException {
156         return getStructure(message).messageStructure;
157     }
158     
159     /**
160      * @returns the message structure from MSH-9-3
161      */
162     private MessageStructure getStructure(String message) throws HL7Exception {
163         EncodingCharacters ec = getEncodingChars(message);
164         String messageStructure;
165         boolean explicityDefined = true;
166         String wholeFieldNine;
167         try {
168             String[] fields = split(message.substring(0, Math.max(message.indexOf(segDelim), message.length())),
169                 String.valueOf(ec.getFieldSeparator()));
170             wholeFieldNine = fields[8];
171             
172             //message structure is component 3 but we'll accept a composite of 1 and 2 if there is no component 3 ...
173             //      if component 1 is ACK, then the structure is ACK regardless of component 2
174             String[] comps = split(wholeFieldNine, String.valueOf(ec.getComponentSeparator()));
175             if (comps.length >= 3) {
176                 messageStructure = comps[2];
177             } else if (comps.length > 0 && comps[0] != null && comps[0].equals("ACK")) {
178                 messageStructure = "ACK";
179             } else if (comps.length == 2) {
180                 explicityDefined = false;
181                 messageStructure = comps[0] + "_" + comps[1];
182             }
183             /*else if (comps.length == 1 && comps[0] != null && comps[0].equals("ACK")) {
184                 messageStructure = "ACK"; //it's common for people to only populate component 1 in an ACK msg
185             }*/
186             else {
187                 String buf = "Can't determine message structure from MSH-9: " + wholeFieldNine +
188                         " HINT: there are only " +
189                         comps.length +
190                         " of 3 components present";
191                 throw new HL7Exception(buf, ErrorCode.UNSUPPORTED_MESSAGE_TYPE);
192             }            
193         }
194         catch (IndexOutOfBoundsException e) {
195             throw new HL7Exception(
196             "Can't find message structure (MSH-9-3): " + e.getMessage(),
197             ErrorCode.UNSUPPORTED_MESSAGE_TYPE);
198         }
199         
200         return new MessageStructure(messageStructure, explicityDefined);
201     }
202     
203     /**
204      * Returns object that contains the field separator and encoding characters
205      * for this message.
206      */
207     private static EncodingCharacters getEncodingChars(String message) {
208         return new EncodingCharacters(message.charAt(3), message.substring(4, 8));
209     }
210     
211     /**
212      * Parses a message string and returns the corresponding Message
213      * object.  Unexpected segments added at the end of their group.  
214      *
215      * @throws HL7Exception if the message is not correctly formatted.
216      * @throws EncodingNotSupportedException if the message encoded
217      *      is not supported by this parser.
218      */
219     protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException {
220         
221         //try to instantiate a message object of the right class
222         MessageStructure structure = getStructure(message);
223         Message m = instantiateMessage(structure.messageStructure, version, structure.explicitlyDefined);
224 
225         parse(m, message);
226 
227         return m;
228     }
229     
230     /**
231      * Parses a segment string and populates the given Segment object.  Unexpected fields are
232      * added as Varies' at the end of the segment.  
233      *
234      * @throws HL7Exception if the given string does not contain the
235      *      given segment or if the string is not encoded properly
236      */
237     public void parse(Segment destination, String segment, EncodingCharacters encodingChars) throws HL7Exception {
238         int fieldOffset = 0;
239         if (isDelimDefSegment(destination.getName())) {
240             fieldOffset = 1;
241             //set field 1 to fourth character of string
242             Terser.set(destination, 1, 0, 1, 1, String.valueOf(encodingChars.getFieldSeparator()));
243         }
244         
245         String[] fields = split(segment, String.valueOf(encodingChars.getFieldSeparator()));
246         //destination.setName(fields[0]);
247         for (int i = 1; i < fields.length; i++) {
248             String[] reps = split(fields[i], String.valueOf(encodingChars.getRepetitionSeparator()));
249             log.debug("{} reps delimited by: {}", reps.length, encodingChars.getRepetitionSeparator());                
250             
251             //MSH-2 will get split incorrectly so we have to fudge it ...
252             boolean isMSH2 = isDelimDefSegment(destination.getName()) && i+fieldOffset == 2;
253             if (isMSH2) {  
254                 reps = new String[1];
255                 reps[0] = fields[i];
256             }
257             
258             for (int j = 0; j < reps.length; j++) {
259                 try {
260                     String statusMessage = "Parsing field " + (i + fieldOffset) +
261                             " repetition " +
262                             j;
263                     log.debug(statusMessage);
264                     //parse(destination.getField(i + fieldOffset, j), reps[j], encodingChars, false);
265 
266                     Type field = destination.getField(i + fieldOffset, j);
267                     if (isMSH2) {
268                         Terser.getPrimitive(field, 1, 1).setValue(reps[j]);
269                     } else {
270                         parse(field, reps[j], encodingChars);
271                     }
272                 }
273                 catch (HL7Exception e) {
274                     //set the field location and throw again ...
275                     e.setFieldPosition(i);
276                     e.setSegmentRepetition(MessageIterator.getIndex(destination.getParent(), destination).rep);
277                     e.setSegmentName(destination.getName());
278                     throw e;
279                 }
280             }
281         }
282         
283         //set data type of OBX-5
284         if (destination.getClass().getName().contains("OBX")) {
285             FixFieldDataType.fixOBX5(destination, getFactory(), getHapiContext().getParserConfiguration());
286         }
287         
288     }
289     
290     /** 
291      * @return true if the segment is MSH, FHS, or BHS.  These need special treatment 
292      *  because they define delimiters.
293      * @param theSegmentName
294      */
295     private static boolean isDelimDefSegment(String theSegmentName) {
296         boolean is = false;
297         if (theSegmentName.equals("MSH") 
298             || theSegmentName.equals("FHS") 
299             || theSegmentName.equals("BHS")) 
300         {
301             is = true;
302         }
303         return is;
304     }
305     
306     /**
307      * Fills a field with values from an unparsed string representing the field.  
308      * @param destinationField the field Type
309      * @param data the field string (including all components and subcomponents; not including field delimiters)
310      * @param encodingCharacters the encoding characters used in the message
311      */
312     public void parse(Type destinationField, String data, EncodingCharacters encodingCharacters) throws HL7Exception {
313         String[] components = split(data, String.valueOf(encodingCharacters.getComponentSeparator()));
314         for (int i = 0; i < components.length; i++) {
315             String[] subcomponents = split(components[i], String.valueOf(encodingCharacters.getSubcomponentSeparator()));
316             for (int j = 0; j < subcomponents.length; j++) {
317                 String val = subcomponents[j];
318                 if (val != null) {
319                     val = Escape.unescape(val, encodingCharacters);
320                 }
321                 Terser.getPrimitive(destinationField, i+1, j+1).setValue(val);                
322             }
323         }
324     }
325     
326     /**
327      * Splits the given composite string into an array of components using the given
328      * delimiter.
329      */
330     public static String[] split(String composite, String delim) {
331         List<String> components = new ArrayList<>();
332         
333         //defend against evil nulls
334         if (composite == null)
335             composite = "";
336         if (delim == null)
337             delim = "";
338         
339         StringTokenizer tok = new StringTokenizer(composite, delim, true);
340         boolean previousTokenWasDelim = true;
341         while (tok.hasMoreTokens()) {
342             String thisTok = tok.nextToken();
343             if (thisTok.equals(delim)) {
344                 if (previousTokenWasDelim)
345                     components.add(null);
346                 previousTokenWasDelim = true;
347             }
348             else {
349                 components.add(thisTok);
350                 previousTokenWasDelim = false;
351             }
352         }
353         
354         return components.toArray(new String[0]);
355     }
356     
357     /**
358      * Encodes the given Type, using the given encoding characters. 
359      * It is assumed that the Type represents a complete field rather than a component.
360      */
361     public static String encode(Type source, EncodingCharacters encodingChars) {
362         StringBuilder field = new StringBuilder();
363         for (int i = 1; i <= Terser.numComponents(source); i++) {
364             StringBuilder comp = new StringBuilder();
365             for (int j = 1; j <= Terser.numSubComponents(source, i); j++) {
366                 Primitive p = Terser.getPrimitive(source, i, j);
367                 comp.append(encodePrimitive(p, encodingChars));
368                 comp.append(encodingChars.getSubcomponentSeparator());
369             }
370             field.append(stripExtraDelimiters(comp.toString(), encodingChars.getSubcomponentSeparator()));
371             field.append(encodingChars.getComponentSeparator());
372         }
373         return stripExtraDelimiters(field.toString(), encodingChars.getComponentSeparator());
374         //return encode(source, encodingChars, false);
375     }
376     
377     private static String encodePrimitive(Primitive p, EncodingCharacters encodingChars) {
378         String val = p.getValue();
379         if (val == null) {
380             val = "";
381         } else {
382             val = Escape.escape(val, encodingChars);
383         }
384         return val;
385     }
386     
387     /**
388      * Removes unecessary delimiters from the end of a field or segment.
389      * This seems to be more convenient than checking to see if they are needed
390      * while we are building the encoded string.
391      */
392     private static String stripExtraDelimiters(String in, char delim) {
393         char[] chars = in.toCharArray();
394         
395         //search from back end for first occurance of non-delimiter ...
396         int c = chars.length - 1;
397         boolean found = false;
398         while (c >= 0 && !found) {
399             if (chars[c--] != delim)
400                 found = true;
401         }
402         
403         String ret = "";
404         if (found)
405             ret = String.valueOf(chars, 0, c + 2);
406         return ret;
407     }
408     
409     /**
410      * Formats a Message object into an HL7 message string using the given
411      * encoding.
412      * @throws HL7Exception if the data fields in the message do not permit encoding
413      *      (e.g. required fields are null)
414      * @throws EncodingNotSupportedException if the requested encoding is not
415      *      supported by this parser.
416      */
417     protected String doEncode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException {
418         if (!this.supportsEncoding(encoding))
419             throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding");
420         
421         return encode(source);
422     }
423     
424     /**
425      * Formats a Message object into an HL7 message string using this parser's
426      * default encoding ("VB").
427      * @throws HL7Exception if the data fields in the message do not permit encoding
428      *      (e.g. required fields are null)
429      */
430     protected String doEncode(Message source) throws HL7Exception {
431         //get encoding characters ...
432         Segment="../../../../ca/uhn/hl7v2/model/Segment.html#Segment">Segment msh = (Segment) source.get("MSH");
433         String fieldSepString = Terser.get(msh, 1, 0, 1, 1);
434         
435         if (fieldSepString == null) 
436             throw new HL7Exception("Can't encode message: MSH-1 (field separator) is missing");
437         
438         char fieldSep = '|';
439         if (fieldSepString.length() > 0)
440             fieldSep = fieldSepString.charAt(0);
441         
442         String encCharString = Terser.get(msh, 2, 0, 1, 1);
443         
444         if (encCharString == null) 
445             throw new HL7Exception("Can't encode message: MSH-2 (encoding characters) is missing");
446                 
447         if (encCharString.length() != 4)
448             throw new HL7Exception(
449             "Encoding characters '" + encCharString + "' invalid -- must be 4 characters",
450             ErrorCode.DATA_TYPE_ERROR);
451         EncodingCharactersrs.html#EncodingCharacters">EncodingCharacters en = new EncodingCharacters(fieldSep, encCharString);
452         
453         //pass down to group encoding method which will operate recursively on children ...
454         return encode(source, en);
455     }
456     
457     /**
458      * Returns given group serialized as a pipe-encoded string - this method is called
459      * by encode(Message source, String encoding).
460      */
461     public static String encode(Group source, EncodingCharacters encodingChars) throws HL7Exception {
462         StringBuilder result = new StringBuilder();
463         
464         String[] names = source.getNames();
465         for (String name : names) {
466             Structure[] reps = source.getAll(name);
467             for (Structure structure : reps) {
468                 if (structure instanceof Group) {
469                     result.append(encode((Group) structure, encodingChars));
470                 } else {
471                     String segString = encode((Segment) structure, encodingChars);
472                     if (segString.length() >= 4) {
473                         result.append(segString);
474                         result.append('\r');
475                     }
476                 }
477             }
478         }
479         return result.toString();
480     }
481     
482     public static String encode(Segment source, EncodingCharacters encodingChars) {
483         StringBuilder result = new StringBuilder();
484         result.append(source.getName());
485         result.append(encodingChars.getFieldSeparator());
486         
487         //start at field 2 for MSH segment because field 1 is the field delimiter
488         int startAt = 1;
489         if (isDelimDefSegment(source.getName()))
490             startAt = 2;
491         
492         //loop through fields; for every field delimit any repetitions and add field delimiter after ...
493         int numFields = source.numFields();
494         for (int i = startAt; i <= numFields; i++) {
495             try {
496                 Type[] reps = source.getField(i);
497                 for (int j = 0; j < reps.length; j++) {
498                     String fieldText = encode(reps[j], encodingChars);
499                     //if this is MSH-2, then it shouldn't be escaped, so unescape it again
500                     if (isDelimDefSegment(source.getName()) && i == 2)
501                         fieldText = Escape.unescape(fieldText, encodingChars);
502                     result.append(fieldText);
503                     if (j < reps.length - 1)
504                         result.append(encodingChars.getRepetitionSeparator());
505                 }
506             }
507             catch (HL7Exception e) {
508                 log.error("Error while encoding segment: ", e);
509             }
510             result.append(encodingChars.getFieldSeparator());
511         }
512         
513         //strip trailing delimiters ...
514         return stripExtraDelimiters(result.toString(), encodingChars.getFieldSeparator());
515     }
516     
517     /**
518      * Removes leading whitespace from the given string.  This method was created to deal with frequent
519      * problems parsing messages that have been hand-written in windows.  The intuitive way to delimit
520      * segments is to hit <ENTER> at the end of each segment, but this creates both a carriage return
521      * and a line feed, so to the parser, the first character of the next segment is the line feed.
522      */
523     public static String stripLeadingWhitespace(String in) {
524         StringBuilder out = new StringBuilder();
525         char[] chars = in.toCharArray();
526         int c = 0;
527         while (c < chars.length) {
528             if (!Character.isWhitespace(chars[c]))
529                 break;
530             c++;
531         }
532         for (int i = c; i < chars.length; i++) {
533             out.append(chars[i]);
534         }
535         return out.toString();
536     }
537     
538     /**
539      * <p>Returns a minimal amount of data from a message string, including only the
540      * data needed to send a response to the remote system.  This includes the
541      * following fields:
542      * <ul><li>field separator</li>
543      * <li>encoding characters</li>
544      * <li>processing ID</li>
545      * <li>message control ID</li></ul>
546      * This method is intended for use when there is an error parsing a message,
547      * (so the Message object is unavailable) but an error message must be sent
548      * back to the remote system including some of the information in the inbound
549      * message.  This method parses only that required information, hopefully
550      * avoiding the condition that caused the original error.  The other
551      * fields in the returned MSH segment are empty.</p>
552      */
553     public Segment getCriticalResponseData(String message) throws HL7Exception {
554         //try to get MSH segment
555         int locStartMSH = message.indexOf("MSH");
556         if (locStartMSH < 0)
557             throw new HL7Exception(
558             "Couldn't find MSH segment in message: " + message,
559             ErrorCode.SEGMENT_SEQUENCE_ERROR);
560         int locEndMSH = message.indexOf('\r', locStartMSH + 1);
561         if (locEndMSH < 0)
562             locEndMSH = message.length();
563         String mshString = message.substring(locStartMSH, locEndMSH);
564         
565         //find out what the field separator is
566         char fieldSep = mshString.charAt(3);
567         
568         //get field array
569         String[] fields = split(mshString, String.valueOf(fieldSep));
570         
571         Segment msh;
572         try {
573             //parse required fields
574             String encChars = fields[1];
575             char compSep = encChars.charAt(0);
576             String messControlID = fields[9];
577             String[] procIDComps = split(fields[10], String.valueOf(compSep));
578             
579             //fill MSH segment
580             String version = Version.lowestAvailableVersion().getVersion(); //default
581             try {
582                 version = this.getVersion(message);
583             }
584             catch (Exception e) { /* use the default */
585             }
586             
587             msh = Parser.makeControlMSH(version, getFactory());
588             Terser.set(msh, 1, 0, 1, 1, String.valueOf(fieldSep));
589             Terser.set(msh, 2, 0, 1, 1, encChars);
590             Terser.set(msh, 10, 0, 1, 1, messControlID);
591             Terser.set(msh, 11, 0, 1, 1, procIDComps[0]);
592             Terser.set(msh, 12, 0, 1, 1, version);
593             
594             }
595         catch (Exception e) {
596             throw new HL7Exception(
597             "Can't parse critical fields from MSH segment ("
598             + e.getClass().getName()
599             + ": "
600             + e.getMessage()
601             + "): "
602             + mshString,
603             ErrorCode.REQUIRED_FIELD_MISSING, e);
604         }
605         
606         return msh;
607     }
608     
609     /**
610      * For response messages, returns the value of MSA-2 (the message ID of the message
611      * sent by the sending system).  This value may be needed prior to main message parsing,
612      * so that (particularly in a multi-threaded scenario) the message can be routed to
613      * the thread that sent the request.  We need this information first so that any
614      * parse exceptions are thrown to the correct thread.
615      * Returns null if MSA-2 can not be found (e.g. if the message is not a
616      * response message).
617      */
618     public String getAckID(String message) {
619         String ackID = null;
620         int startMSA = message.indexOf("\rMSA");
621         if (startMSA >= 0) {
622             int startFieldOne = startMSA + 5;
623             char fieldDelim = message.charAt(startFieldOne - 1);
624             int start = message.indexOf(fieldDelim, startFieldOne) + 1;
625             int end = message.indexOf(fieldDelim, start);
626             int segEnd = message.indexOf(segDelim, start);
627             if (segEnd > start && segEnd < end)
628                 end = segEnd;
629             
630             //if there is no field delim after MSH-2, need to go to end of message, but not including end seg delim if it exists
631             if (end < 0) {
632                 if (message.charAt(message.length() - 1) == '\r') {
633                     end = message.length() - 1;
634                 }
635                 else {
636                     end = message.length();
637                 }
638             }
639             if (start > 0 && end > start) {
640                 ackID = message.substring(start, end);
641             }
642         }
643         log.debug("ACK ID: {}", ackID);
644         return ackID;
645     }
646     
647     /**
648      * Returns the version ID (MSH-12) from the given message, without fully parsing the message.
649      * The version is needed prior to parsing in order to determine the message class
650      * into which the text of the message should be parsed.
651      * @throws HL7Exception if the version field can not be found.
652      */
653     public String getVersion(String message) throws HL7Exception {
654         int startMSH = message.indexOf("MSH");
655         int endMSH = message.indexOf(OldPipeParser.segDelim, startMSH);
656         if (endMSH < 0)
657             endMSH = message.length();
658         String msh = message.substring(startMSH, endMSH);
659         String fieldSep;
660         if (msh.length() > 3) {
661             fieldSep = String.valueOf(msh.charAt(3));
662         }
663         else {
664             throw new HL7Exception("Can't find field separator in MSH: " + msh, ErrorCode.UNSUPPORTED_VERSION_ID);
665         }
666         
667         String[] fields = split(msh, fieldSep);
668         
669         String compSep;
670         if (fields.length >= 2 && fields[1] != null && fields[1].length() == 4) {
671             compSep = String.valueOf(fields[1].charAt(0)); //get component separator as 1st encoding char
672         } 
673         else {
674             throw new HL7Exception("Invalid or incomplete encoding characters - MSH-2 is " + fields[1],  
675                     ErrorCode.REQUIRED_FIELD_MISSING);
676         }
677         
678         String version;
679         if (fields.length >= 12) {
680         	String[] comp = split(fields[11], compSep);
681         	if (comp.length >= 1) {
682         		version = comp[0];
683         	} else {
684         		throw new HL7Exception("Can't find version ID - MSH.12 is " + fields[11],
685         				ErrorCode.REQUIRED_FIELD_MISSING);
686         	}
687         }
688         else {
689             throw new HL7Exception(
690             		"Can't find version ID - MSH has only " + fields.length + " fields.",
691             		ErrorCode.REQUIRED_FIELD_MISSING);
692         }
693         return version;
694     }
695 
696     /**
697      * {@inheritDoc }
698      */
699     public String doEncode(Segment structure, EncodingCharacters encodingCharacters) {
700         return encode(structure, encodingCharacters);
701     }
702 
703     /**
704      * {@inheritDoc }
705      */
706     public String doEncode(Type type, EncodingCharacters encodingCharacters) {
707         return encode(type, encodingCharacters);
708     }
709 
710     /**
711      * Throws unsupported operation exception
712      *
713      * @throws UnsupportedOperationException
714      */
715     @Override
716 	protected Message doParseForSpecificPackage(String theMessage, String theVersion, String thePackageName) {
717         throw new UnsupportedOperationException("Not supported yet.");
718 	}
719     
720     public void parse(Message message, String string) throws HL7Exception {
721         MessageIteratoressageIterator">MessageIterator messageIter = new MessageIterator(message, "MSH", true);
722         FilterIterator.Predicate<Structure> segmentsOnly = obj -> Segment.class.isAssignableFrom(obj.getClass());
723         FilterIterator<Structure> segmentIter = new FilterIterator<>(messageIter, segmentsOnly);
724 
725         String[] segments = split(string, segDelim);
726 
727         char delim = '|';
728         for (int i = 0; i < segments.length; i++) {
729 
730             //get rid of any leading whitespace characters ...
731             if (segments[i] != null && segments[i].length() > 0 && Character.isWhitespace(segments[i].charAt(0)))
732                 segments[i] = stripLeadingWhitespace(segments[i]);
733 
734             //sometimes people put extra segment delimiters at end of msg ...
735             if (segments[i] != null && segments[i].length() >= 3) {
736                 final String name;
737                 if (i == 0) {
738                     name = segments[i].substring(0, 3);
739                     delim = segments[i].charAt(3);
740                 } else {
741                     if (segments[i].indexOf(delim) >= 0 ) {
742                         name = segments[i].substring(0, segments[i].indexOf(delim));
743                       } else {
744                         name = segments[i];
745                       }
746                  }
747 
748                 log.debug("Parsing segment {}", name);
749 
750                 messageIter.setDirection(name);
751                 FilterIterator.Predicate<Structure> byDirection = obj -> {
752                     log.debug("PipeParser iterating message in direction {} at {} ", name, obj.getName());
753                     return obj.getName().matches(name + "\\d*");
754                 };
755                 FilterIterator<Structure> dirIter = new FilterIterator<>(segmentIter, byDirection);
756                 if (dirIter.hasNext()) {
757                     parse((Segment) dirIter.next(), segments[i], getEncodingChars(string));
758                 }
759             }
760         }
761     }
762 
763     
764     /**
765      * A struct for holding a message class string and a boolean indicating whether it 
766      * was defined explicitly.  
767      */
768     private static class MessageStructure {
769         public final String messageStructure;
770         public final boolean explicitlyDefined;
771         
772         public MessageStructure(String theMessageStructure, boolean isExplicitlyDefined) {
773             messageStructure = theMessageStructure;
774             explicitlyDefined = isExplicitlyDefined;
775         }
776     }
777     
778 }