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