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.StringTokenizer;
032    
033    import ca.uhn.hl7v2.HL7Exception;
034    import ca.uhn.hl7v2.model.Group;
035    import ca.uhn.hl7v2.model.Message;
036    import ca.uhn.hl7v2.model.Primitive;
037    import ca.uhn.hl7v2.model.Segment;
038    import ca.uhn.hl7v2.model.Structure;
039    import ca.uhn.hl7v2.model.Type;
040    import ca.uhn.hl7v2.model.Varies;
041    import ca.uhn.hl7v2.util.Terser;
042    import ca.uhn.hl7v2.util.MessageIterator;
043    import ca.uhn.hl7v2.util.FilterIterator;
044    import ca.uhn.log.HapiLog;
045    import ca.uhn.log.HapiLogFactory;
046    
047    /**
048     * This is a legacy implementation of the PipeParser and should not be used
049     * for new projects.
050     *
051     * In version 1.0 of HAPI, a behaviour was corrected where unexpected segments
052     * would be placed at the tail end of the first segment group encountered. Any
053     * legacy code which still depends on previous behaviour can use this
054     * implementation.
055     *
056     * @author Bryan Tripp (bryan_tripp@sourceforge.net)
057     * @deprecated
058     */
059    public class OldPipeParser extends Parser {
060        
061        private static final HapiLog log = HapiLogFactory.getHapiLog(PipeParser.class);
062        
063        private final static String segDelim = "\r"; //see section 2.8 of spec
064        
065        /** Creates a new PipeParser */
066        public OldPipeParser() {
067        }
068    
069        /** 
070         * Creates a new PipeParser 
071         *  
072         * @param theFactory custom factory to use for model class lookup 
073         */
074        public OldPipeParser(ModelClassFactory theFactory) {
075            super(theFactory);
076        }
077        
078        /**
079         * Returns a String representing the encoding of the given message, if
080         * the encoding is recognized.  For example if the given message appears
081         * to be encoded using HL7 2.x XML rules then "XML" would be returned.
082         * If the encoding is not recognized then null is returned.  That this
083         * method returns a specific encoding does not guarantee that the
084         * message is correctly encoded (e.g. well formed XML) - just that
085         * it is not encoded using any other encoding than the one returned.
086         */
087        public String getEncoding(String message) {
088            String encoding = null;
089            
090            //quit if the string is too short
091            if (message.length() < 4)
092                return null;
093            
094            //see if it looks like this message is | encoded ...
095            boolean ok = true;
096            
097            //string should start with "MSH"
098            if (!message.startsWith("MSH"))
099                return null;
100            
101            //4th character of each segment should be field delimiter
102            char fourthChar = message.charAt(3);
103            StringTokenizer st = new StringTokenizer(message, String.valueOf(segDelim), false);
104            while (st.hasMoreTokens()) {
105                String x = st.nextToken();
106                if (x.length() > 0) {
107                    if (Character.isWhitespace(x.charAt(0)))
108                        x = stripLeadingWhitespace(x);
109                    if (x.length() >= 4 && x.charAt(3) != fourthChar)
110                        return null;
111                }
112            }
113            
114            //should be at least 11 field delimiters (because MSH-12 is required)
115            int nextFieldDelimLoc = 0;
116            for (int i = 0; i < 11; i++) {
117                nextFieldDelimLoc = message.indexOf(fourthChar, nextFieldDelimLoc + 1);
118                if (nextFieldDelimLoc < 0)
119                    return null;
120            }
121            
122            if (ok)
123                encoding = "VB";
124            
125            return encoding;
126        }
127        
128        /**
129         * @return the preferred encoding of this Parser
130         */
131        public String getDefaultEncoding() {
132            return "VB";
133        }
134        
135        /**
136         * Returns true if and only if the given encoding is supported
137         * by this Parser.
138         */
139        public boolean supportsEncoding(String encoding) {
140            boolean supports = false;
141            if (encoding != null && encoding.equals("VB"))
142                supports = true;
143            return supports;
144        }
145        
146        /**
147         * @deprecated this method should not be public 
148         * @param message
149         * @return
150         * @throws HL7Exception
151         * @throws EncodingNotSupportedException
152         */
153        public String getMessageStructure(String message) throws HL7Exception, EncodingNotSupportedException {
154            return getStructure(message).messageStructure;
155        }
156        
157        /**
158         * @returns the message structure from MSH-9-3
159         */
160        private MessageStructure getStructure(String message) throws HL7Exception, EncodingNotSupportedException {
161            EncodingCharacters ec = getEncodingChars(message);
162            String messageStructure = null;
163            boolean explicityDefined = true;
164            String wholeFieldNine;
165            try {
166                String[] fields = split(message.substring(0, Math.max(message.indexOf(segDelim), message.length())),
167                    String.valueOf(ec.getFieldSeparator()));
168                wholeFieldNine = fields[8];
169                
170                //message structure is component 3 but we'll accept a composite of 1 and 2 if there is no component 3 ...
171                //      if component 1 is ACK, then the structure is ACK regardless of component 2
172                String[] comps = split(wholeFieldNine, String.valueOf(ec.getComponentSeparator()));
173                if (comps.length >= 3) {
174                    messageStructure = comps[2];
175                } else if (comps.length > 0 && comps[0] != null && comps[0].equals("ACK")) {
176                    messageStructure = "ACK";
177                } else if (comps.length == 2) {
178                    explicityDefined = false;
179                    messageStructure = comps[0] + "_" + comps[1];
180                }
181                /*else if (comps.length == 1 && comps[0] != null && comps[0].equals("ACK")) {
182                    messageStructure = "ACK"; //it's common for people to only populate component 1 in an ACK msg
183                }*/
184                else {
185                    StringBuffer buf = new StringBuffer("Can't determine message structure from MSH-9: ");
186                    buf.append(wholeFieldNine);
187                    if (comps.length < 3) {
188                        buf.append(" HINT: there are only ");
189                        buf.append(comps.length);
190                        buf.append(" of 3 components present");
191                    }
192                    throw new HL7Exception(buf.toString(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE);
193                }            
194            }
195            catch (IndexOutOfBoundsException e) {
196                throw new HL7Exception(
197                "Can't find message structure (MSH-9-3): " + e.getMessage(),
198                HL7Exception.UNSUPPORTED_MESSAGE_TYPE);
199            }
200            
201            return new MessageStructure(messageStructure, explicityDefined);
202        }
203        
204        /**
205         * Returns object that contains the field separator and encoding characters
206         * for this message.
207         */
208        private static EncodingCharacters getEncodingChars(String message) {
209            return new EncodingCharacters(message.charAt(3), message.substring(4, 8));
210        }
211        
212        /**
213         * Parses a message string and returns the corresponding Message
214         * object.  Unexpected segments added at the end of their group.  
215         *
216         * @throws HL7Exception if the message is not correctly formatted.
217         * @throws EncodingNotSupportedException if the message encoded
218         *      is not supported by this parser.
219         */
220        protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException {
221            
222            //try to instantiate a message object of the right class
223            MessageStructure structure = getStructure(message);
224            Message m = instantiateMessage(structure.messageStructure, version, structure.explicitlyDefined);
225    
226            parse(m, message);
227    
228            return m;
229        }
230        
231        /**
232         * Parses a segment string and populates the given Segment object.  Unexpected fields are
233         * added as Varies' at the end of the segment.  
234         *
235         * @throws HL7Exception if the given string does not contain the
236         *      given segment or if the string is not encoded properly
237         */
238        public void parse(Segment destination, String segment, EncodingCharacters encodingChars) throws HL7Exception {
239            int fieldOffset = 0;
240            if (isDelimDefSegment(destination.getName())) {
241                fieldOffset = 1;
242                //set field 1 to fourth character of string
243                Terser.set(destination, 1, 0, 1, 1, String.valueOf(encodingChars.getFieldSeparator()));
244            }
245            
246            String[] fields = split(segment, String.valueOf(encodingChars.getFieldSeparator()));
247            //destination.setName(fields[0]);
248            for (int i = 1; i < fields.length; i++) {
249                String[] reps = split(fields[i], String.valueOf(encodingChars.getRepetitionSeparator()));
250                if (log.isDebugEnabled()) {
251                    log.debug(reps.length + "reps delimited by: " + encodingChars.getRepetitionSeparator());                
252                }
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        /** Returns the component or subcomponent separator from the given encoding characters. */
331        private static char getSeparator(boolean subComponents, EncodingCharacters encodingChars) {
332            char separator;
333            if (subComponents) {
334                separator = encodingChars.getSubcomponentSeparator();
335            }
336            else {
337                separator = encodingChars.getComponentSeparator();
338            }
339            return separator;
340        }
341        
342        /**
343         * Splits the given composite string into an array of components using the given
344         * delimiter.
345         */
346        public static String[] split(String composite, String delim) {
347            ArrayList components = new ArrayList();
348            
349            //defend against evil nulls
350            if (composite == null)
351                composite = "";
352            if (delim == null)
353                delim = "";
354            
355            StringTokenizer tok = new StringTokenizer(composite, delim, true);
356            boolean previousTokenWasDelim = true;
357            while (tok.hasMoreTokens()) {
358                String thisTok = tok.nextToken();
359                if (thisTok.equals(delim)) {
360                    if (previousTokenWasDelim)
361                        components.add(null);
362                    previousTokenWasDelim = true;
363                }
364                else {
365                    components.add(thisTok);
366                    previousTokenWasDelim = false;
367                }
368            }
369            
370            String[] ret = new String[components.size()];
371            for (int i = 0; i < components.size(); i++) {
372                ret[i] = (String) components.get(i);
373            }
374            
375            return ret;
376        }
377        
378        /**
379         * Encodes the given Type, using the given encoding characters. 
380         * It is assumed that the Type represents a complete field rather than a component.
381         */
382        public static String encode(Type source, EncodingCharacters encodingChars) {
383            StringBuffer field = new StringBuffer();
384            for (int i = 1; i <= Terser.numComponents(source); i++) {
385                StringBuffer comp = new StringBuffer();
386                for (int j = 1; j <= Terser.numSubComponents(source, i); j++) {
387                    Primitive p = Terser.getPrimitive(source, i, j);
388                    comp.append(encodePrimitive(p, encodingChars));
389                    comp.append(encodingChars.getSubcomponentSeparator());
390                }
391                field.append(stripExtraDelimiters(comp.toString(), encodingChars.getSubcomponentSeparator()));
392                field.append(encodingChars.getComponentSeparator());
393            }
394            return stripExtraDelimiters(field.toString(), encodingChars.getComponentSeparator());
395            //return encode(source, encodingChars, false);
396        }
397        
398        private static String encodePrimitive(Primitive p, EncodingCharacters encodingChars) {
399            String val = ((Primitive) p).getValue();
400            if (val == null) {
401                val = "";
402            } else {
403                val = Escape.escape(val, encodingChars);
404            }
405            return val;
406        }
407        
408        /**
409         * Removes unecessary delimiters from the end of a field or segment.
410         * This seems to be more convenient than checking to see if they are needed
411         * while we are building the encoded string.
412         */
413        private static String stripExtraDelimiters(String in, char delim) {
414            char[] chars = in.toCharArray();
415            
416            //search from back end for first occurance of non-delimiter ...
417            int c = chars.length - 1;
418            boolean found = false;
419            while (c >= 0 && !found) {
420                if (chars[c--] != delim)
421                    found = true;
422            }
423            
424            String ret = "";
425            if (found)
426                ret = String.valueOf(chars, 0, c + 2);
427            return ret;
428        }
429        
430        /**
431         * Formats a Message object into an HL7 message string using the given
432         * encoding.
433         * @throws HL7Exception if the data fields in the message do not permit encoding
434         *      (e.g. required fields are null)
435         * @throws EncodingNotSupportedException if the requested encoding is not
436         *      supported by this parser.
437         */
438        protected String doEncode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException {
439            if (!this.supportsEncoding(encoding))
440                throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding");
441            
442            return encode(source);
443        }
444        
445        /**
446         * Formats a Message object into an HL7 message string using this parser's
447         * default encoding ("VB").
448         * @throws HL7Exception if the data fields in the message do not permit encoding
449         *      (e.g. required fields are null)
450         */
451        protected String doEncode(Message source) throws HL7Exception {
452            //get encoding characters ...
453            Segment msh = (Segment) source.get("MSH");
454            String fieldSepString = Terser.get(msh, 1, 0, 1, 1);
455            
456            if (fieldSepString == null) 
457                throw new HL7Exception("Can't encode message: MSH-1 (field separator) is missing");
458            
459            char fieldSep = '|';
460            if (fieldSepString != null && fieldSepString.length() > 0)
461                fieldSep = fieldSepString.charAt(0);
462            
463            String encCharString = Terser.get(msh, 2, 0, 1, 1);
464            
465            if (encCharString == null) 
466                throw new HL7Exception("Can't encode message: MSH-2 (encoding characters) is missing");
467                    
468            if (encCharString.length() != 4)
469                throw new HL7Exception(
470                "Encoding characters '" + encCharString + "' invalid -- must be 4 characters",
471                HL7Exception.DATA_TYPE_ERROR);
472            EncodingCharacters en = new EncodingCharacters(fieldSep, encCharString);
473            
474            //pass down to group encoding method which will operate recursively on children ...
475            return encode((Group) source, en);
476        }
477        
478        /**
479         * Returns given group serialized as a pipe-encoded string - this method is called
480         * by encode(Message source, String encoding).
481         */
482        public static String encode(Group source, EncodingCharacters encodingChars) throws HL7Exception {
483            StringBuffer result = new StringBuffer();
484            
485            String[] names = source.getNames();
486            for (int i = 0; i < names.length; i++) {
487                Structure[] reps = source.getAll(names[i]);
488                for (int rep = 0; rep < reps.length; rep++) {
489                    if (reps[rep] instanceof Group) {
490                        result.append(encode((Group) reps[rep], encodingChars));
491                    }
492                    else {
493                        String segString = encode((Segment) reps[rep], encodingChars);
494                        if (segString.length() >= 4) {
495                            result.append(segString);
496                            result.append('\r');
497                        }
498                    }
499                }
500            }
501            return result.toString();
502        }
503        
504        public static String encode(Segment source, EncodingCharacters encodingChars) {
505            StringBuffer result = new StringBuffer();
506            result.append(source.getName());
507            result.append(encodingChars.getFieldSeparator());
508            
509            //start at field 2 for MSH segment because field 1 is the field delimiter
510            int startAt = 1;
511            if (isDelimDefSegment(source.getName()))
512                startAt = 2;
513            
514            //loop through fields; for every field delimit any repetitions and add field delimiter after ...
515            int numFields = source.numFields();
516            for (int i = startAt; i <= numFields; i++) {
517                try {
518                    Type[] reps = source.getField(i);
519                    for (int j = 0; j < reps.length; j++) {
520                        String fieldText = encode(reps[j], encodingChars);
521                        //if this is MSH-2, then it shouldn't be escaped, so unescape it again
522                        if (isDelimDefSegment(source.getName()) && i == 2)
523                            fieldText = Escape.unescape(fieldText, encodingChars);
524                        result.append(fieldText);
525                        if (j < reps.length - 1)
526                            result.append(encodingChars.getRepetitionSeparator());
527                    }
528                }
529                catch (HL7Exception e) {
530                    log.error("Error while encoding segment: ", e);
531                }
532                result.append(encodingChars.getFieldSeparator());
533            }
534            
535            //strip trailing delimiters ...
536            return stripExtraDelimiters(result.toString(), encodingChars.getFieldSeparator());
537        }
538        
539        /**
540         * Removes leading whitespace from the given string.  This method was created to deal with frequent
541         * problems parsing messages that have been hand-written in windows.  The intuitive way to delimit
542         * segments is to hit <ENTER> at the end of each segment, but this creates both a carriage return
543         * and a line feed, so to the parser, the first character of the next segment is the line feed.
544         */
545        public static String stripLeadingWhitespace(String in) {
546            StringBuffer out = new StringBuffer();
547            char[] chars = in.toCharArray();
548            int c = 0;
549            while (c < chars.length) {
550                if (!Character.isWhitespace(chars[c]))
551                    break;
552                c++;
553            }
554            for (int i = c; i < chars.length; i++) {
555                out.append(chars[i]);
556            }
557            return out.toString();
558        }
559        
560        /**
561         * <p>Returns a minimal amount of data from a message string, including only the
562         * data needed to send a response to the remote system.  This includes the
563         * following fields:
564         * <ul><li>field separator</li>
565         * <li>encoding characters</li>
566         * <li>processing ID</li>
567         * <li>message control ID</li></ul>
568         * This method is intended for use when there is an error parsing a message,
569         * (so the Message object is unavailable) but an error message must be sent
570         * back to the remote system including some of the information in the inbound
571         * message.  This method parses only that required information, hopefully
572         * avoiding the condition that caused the original error.  The other
573         * fields in the returned MSH segment are empty.</p>
574         */
575        public Segment getCriticalResponseData(String message) throws HL7Exception {
576            //try to get MSH segment
577            int locStartMSH = message.indexOf("MSH");
578            if (locStartMSH < 0)
579                throw new HL7Exception(
580                "Couldn't find MSH segment in message: " + message,
581                HL7Exception.SEGMENT_SEQUENCE_ERROR);
582            int locEndMSH = message.indexOf('\r', locStartMSH + 1);
583            if (locEndMSH < 0)
584                locEndMSH = message.length();
585            String mshString = message.substring(locStartMSH, locEndMSH);
586            
587            //find out what the field separator is
588            char fieldSep = mshString.charAt(3);
589            
590            //get field array
591            String[] fields = split(mshString, String.valueOf(fieldSep));
592            
593            Segment msh = null;
594            try {
595                //parse required fields
596                String encChars = fields[1];
597                char compSep = encChars.charAt(0);
598                String messControlID = fields[9];
599                String[] procIDComps = split(fields[10], String.valueOf(compSep));
600                
601                //fill MSH segment
602                String version = "2.4"; //default
603                try {
604                    version = this.getVersion(message);
605                }
606                catch (Exception e) { /* use the default */
607                }
608                
609                msh = Parser.makeControlMSH(version, getFactory());
610                Terser.set(msh, 1, 0, 1, 1, String.valueOf(fieldSep));
611                Terser.set(msh, 2, 0, 1, 1, encChars);
612                Terser.set(msh, 10, 0, 1, 1, messControlID);
613                Terser.set(msh, 11, 0, 1, 1, procIDComps[0]);
614                Terser.set(msh, 12, 0, 1, 1, version);
615                
616                }
617            catch (Exception e) {
618                throw new HL7Exception(
619                "Can't parse critical fields from MSH segment ("
620                + e.getClass().getName()
621                + ": "
622                + e.getMessage()
623                + "): "
624                + mshString,
625                HL7Exception.REQUIRED_FIELD_MISSING, e);
626            }
627            
628            return msh;
629        }
630        
631        /**
632         * For response messages, returns the value of MSA-2 (the message ID of the message
633         * sent by the sending system).  This value may be needed prior to main message parsing,
634         * so that (particularly in a multi-threaded scenario) the message can be routed to
635         * the thread that sent the request.  We need this information first so that any
636         * parse exceptions are thrown to the correct thread.
637         * Returns null if MSA-2 can not be found (e.g. if the message is not a
638         * response message).
639         */
640        public String getAckID(String message) {
641            String ackID = null;
642            int startMSA = message.indexOf("\rMSA");
643            if (startMSA >= 0) {
644                int startFieldOne = startMSA + 5;
645                char fieldDelim = message.charAt(startFieldOne - 1);
646                int start = message.indexOf(fieldDelim, startFieldOne) + 1;
647                int end = message.indexOf(fieldDelim, start);
648                int segEnd = message.indexOf(String.valueOf(segDelim), start);
649                if (segEnd > start && segEnd < end)
650                    end = segEnd;
651                
652                //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
653                if (end < 0) {
654                    if (message.charAt(message.length() - 1) == '\r') {
655                        end = message.length() - 1;
656                    }
657                    else {
658                        end = message.length();
659                    }
660                }
661                if (start > 0 && end > start) {
662                    ackID = message.substring(start, end);
663                }
664            }
665            log.debug("ACK ID: " + ackID);
666            return ackID;
667        }
668        
669        /**
670         * Returns the version ID (MSH-12) from the given message, without fully parsing the message.
671         * The version is needed prior to parsing in order to determine the message class
672         * into which the text of the message should be parsed.
673         * @throws HL7Exception if the version field can not be found.
674         */
675        public String getVersion(String message) throws HL7Exception {
676            int startMSH = message.indexOf("MSH");
677            int endMSH = message.indexOf(OldPipeParser.segDelim, startMSH);
678            if (endMSH < 0)
679                endMSH = message.length();
680            String msh = message.substring(startMSH, endMSH);
681            String fieldSep = null;
682            if (msh.length() > 3) {
683                fieldSep = String.valueOf(msh.charAt(3));
684            }
685            else {
686                throw new HL7Exception("Can't find field separator in MSH: " + msh, HL7Exception.UNSUPPORTED_VERSION_ID);
687            }
688            
689            String[] fields = split(msh, fieldSep);
690            
691            String compSep = null;
692            if (fields.length >= 2 && fields[1] != null && fields[1].length() == 4) {
693                compSep = String.valueOf(fields[1].charAt(0)); //get component separator as 1st encoding char
694            } 
695            else {
696                throw new HL7Exception("Invalid or incomplete encoding characters - MSH-2 is " + fields[1],  
697                        HL7Exception.REQUIRED_FIELD_MISSING);
698            }
699            
700            String version = null;
701            if (fields.length >= 12) {
702                    String[] comp = split(fields[11], compSep);
703                    if (comp.length >= 1) {
704                            version = comp[0];
705                    } else {
706                            throw new HL7Exception("Can't find version ID - MSH.12 is " + fields[11],
707                                            HL7Exception.REQUIRED_FIELD_MISSING);
708                    }
709            }
710            else {
711                throw new HL7Exception(
712                "Can't find version ID - MSH has only " + fields.length + " fields.",
713                HL7Exception.REQUIRED_FIELD_MISSING);
714            }
715            return version;
716        }
717    
718        /**
719         * {@inheritDoc }
720         */
721        public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception {
722            return encode(structure, encodingCharacters);
723        }
724    
725        /**
726         * {@inheritDoc }
727         */
728        public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception {
729            return encode(type, encodingCharacters);
730        }
731    
732        public void parse(Message message, String string) throws HL7Exception {
733            //MessagePointer ptr = new MessagePointer(this, m, getEncodingChars(message));
734            MessageIterator messageIter = new MessageIterator(message, "MSH", true);
735            FilterIterator.Predicate segmentsOnly = new FilterIterator.Predicate() {
736                public boolean evaluate(Object obj) {
737                    if (Segment.class.isAssignableFrom(obj.getClass())) {
738                        return true;
739                    } else {
740                        return false;
741                    }
742                }
743            };
744            FilterIterator segmentIter = new FilterIterator(messageIter, segmentsOnly);
745    
746            String[] segments = split(string, segDelim);
747    
748            char delim = '|';
749            for (int i = 0; i < segments.length; i++) {
750    
751                //get rid of any leading whitespace characters ...
752                if (segments[i] != null && segments[i].length() > 0 && Character.isWhitespace(segments[i].charAt(0)))
753                    segments[i] = stripLeadingWhitespace(segments[i]);
754    
755                //sometimes people put extra segment delimiters at end of msg ...
756                if (segments[i] != null && segments[i].length() >= 3) {
757                    final String name;
758                    if (i == 0) {
759                        name = segments[i].substring(0, 3);
760                        delim = segments[i].charAt(3);
761                    } else {
762                        if (segments[i].indexOf(delim) >= 0 ) {
763                            name = segments[i].substring(0, segments[i].indexOf(delim));
764                          } else {
765                            name = segments[i];
766                          }
767                     }
768    
769                    log.debug("Parsing segment " + name);
770    
771                    messageIter.setDirection(name);
772                    FilterIterator.Predicate byDirection = new FilterIterator.Predicate() {
773                        public boolean evaluate(Object obj) {
774                            Structure s = (Structure) obj;
775                            log.debug("PipeParser iterating message in direction " + name + " at " + s.getName());
776                            if (s.getName().matches(name + "\\d*")) {
777                                return true;
778                            } else {
779                                return false;
780                            }
781                        }
782                    };
783                    FilterIterator dirIter = new FilterIterator(segmentIter, byDirection);
784                    if (dirIter.hasNext()) {
785                        parse((Segment) dirIter.next(), segments[i], getEncodingChars(string));
786                    }
787                }
788            }
789        }
790    
791        
792        /**
793         * A struct for holding a message class string and a boolean indicating whether it 
794         * was defined explicitly.  
795         */
796        private static class MessageStructure {
797            public String messageStructure;
798            public boolean explicitlyDefined;
799            
800            public MessageStructure(String theMessageStructure, boolean isExplicitlyDefined) {
801                messageStructure = theMessageStructure;
802                explicitlyDefined = isExplicitlyDefined;
803            }
804        }
805        
806    }