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.ReflectionUtil;
043    import ca.uhn.log.HapiLog;
044    import ca.uhn.log.HapiLogFactory;
045    import java.util.HashMap;
046    
047    /**
048     * An implementation of Parser that supports traditionally encoded (ie delimited
049     * with characters like |, ^, and ~) HL7 messages. Unexpected segments and
050     * fields are parsed into generic elements that are added to the message.
051     * 
052     * @author Bryan Tripp (bryan_tripp@sourceforge.net)
053     */
054    public class PipeParser extends Parser {
055    
056        private static final HapiLog log = HapiLogFactory.getHapiLog(PipeParser.class);
057    
058        private final static String segDelim = "\r"; // see section 2.8 of spec
059    
060        private final HashMap<Class<? extends Message>, StructureDefinition> myStructureDefinitions = new HashMap<Class<? extends Message>, StructureDefinition>();
061    
062        /**
063         * System property key. If value is "true", legacy mode will default to true
064         * 
065         * @see #isLegacyMode()
066         */
067        public static final String DEFAULT_LEGACY_MODE_PROPERTY = "ca.uhn.hl7v2.parser.PipeParser.default_legacy_mode";
068    
069        private Boolean myLegacyMode = null;
070    
071    
072        /** Creates a new PipeParser */
073        public PipeParser() {
074        }
075    
076    
077        /**
078         * Creates a new PipeParser
079         * 
080         * @param theFactory
081         *            custom factory to use for model class lookup
082         */
083        public PipeParser(ModelClassFactory theFactory) {
084            super(theFactory);
085        }
086    
087    
088        /**
089         * Returns a String representing the encoding of the given message, if the
090         * encoding is recognized. For example if the given message appears to be
091         * encoded using HL7 2.x XML rules then "XML" would be returned. If the
092         * encoding is not recognized then null is returned. That this method
093         * returns a specific encoding does not guarantee that the message is
094         * correctly encoded (e.g. well formed XML) - just that it is not encoded
095         * using any other encoding than the one returned.
096         */
097        public String getEncoding(String message) {
098            String encoding = null;
099    
100            // quit if the string is too short
101            if (message.length() < 4)
102                return null;
103    
104            // see if it looks like this message is | encoded ...
105            boolean ok = true;
106    
107            // string should start with "MSH"
108            if (!message.startsWith("MSH"))
109                return null;
110    
111            // 4th character of each segment should be field delimiter
112            char fourthChar = message.charAt(3);
113            StringTokenizer st = new StringTokenizer(message, String.valueOf(segDelim), false);
114            while (st.hasMoreTokens()) {
115                String x = st.nextToken();
116                if (x.length() > 0) {
117                    if (Character.isWhitespace(x.charAt(0)))
118                        x = stripLeadingWhitespace(x);
119                    if (x.length() >= 4 && x.charAt(3) != fourthChar)
120                        return null;
121                }
122            }
123    
124            // should be at least 11 field delimiters (because MSH-12 is required)
125            int nextFieldDelimLoc = 0;
126            for (int i = 0; i < 11; i++) {
127                nextFieldDelimLoc = message.indexOf(fourthChar, nextFieldDelimLoc + 1);
128                if (nextFieldDelimLoc < 0)
129                    return null;
130            }
131    
132            if (ok)
133                encoding = "VB";
134    
135            return encoding;
136        }
137    
138    
139        /**
140         * @return the preferred encoding of this Parser
141         */
142        public String getDefaultEncoding() {
143            return "VB";
144        }
145    
146    
147        /**
148         * Returns true if and only if the given encoding is supported by this
149         * Parser.
150         */
151        public boolean supportsEncoding(String encoding) {
152            boolean supports = false;
153            if (encoding != null && encoding.equals("VB"))
154                supports = true;
155            return supports;
156        }
157    
158    
159        /**
160         * @deprecated this method should not be public
161         * @param message
162         * @return
163         * @throws HL7Exception
164         * @throws EncodingNotSupportedException
165         */
166        public String getMessageStructure(String message) throws HL7Exception, EncodingNotSupportedException {
167            return getStructure(message).messageStructure;
168        }
169    
170    
171        /**
172         * @returns the message structure from MSH-9-3
173         */
174        private MessageStructure getStructure(String message) throws HL7Exception, EncodingNotSupportedException {
175            EncodingCharacters ec = getEncodingChars(message);
176            String messageStructure = null;
177            boolean explicityDefined = true;
178            String wholeFieldNine;
179            try {
180                String[] fields = split(message.substring(0, Math.max(message.indexOf(segDelim), message.length())), String.valueOf(ec.getFieldSeparator()));
181                wholeFieldNine = fields[8];
182    
183                // message structure is component 3 but we'll accept a composite of
184                // 1 and 2 if there is no component 3 ...
185                // if component 1 is ACK, then the structure is ACK regardless of
186                // component 2
187                String[] comps = split(wholeFieldNine, String.valueOf(ec.getComponentSeparator()));
188                if (comps.length >= 3) {
189                    messageStructure = comps[2];
190                } else if (comps.length > 0 && comps[0] != null && comps[0].equals("ACK")) {
191                    messageStructure = "ACK";
192                } else if (comps.length == 2) {
193                    explicityDefined = false;
194                    messageStructure = comps[0] + "_" + comps[1];
195                }
196                /*
197                 * else if (comps.length == 1 && comps[0] != null &&
198                 * comps[0].equals("ACK")) { messageStructure = "ACK"; //it's common
199                 * for people to only populate component 1 in an ACK msg }
200                 */
201                else {
202                    StringBuffer buf = new StringBuffer("Can't determine message structure from MSH-9: ");
203                    buf.append(wholeFieldNine);
204                    if (comps.length < 3) {
205                        buf.append(" HINT: there are only ");
206                        buf.append(comps.length);
207                        buf.append(" of 3 components present");
208                    }
209                    throw new HL7Exception(buf.toString(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE);
210                }
211            } catch (IndexOutOfBoundsException e) {
212                throw new HL7Exception("Can't find message structure (MSH-9-3): " + e.getMessage(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE);
213            }
214    
215            return new MessageStructure(messageStructure, explicityDefined);
216        }
217    
218    
219        /**
220         * Returns object that contains the field separator and encoding characters
221         * for this message.
222         */
223        private static EncodingCharacters getEncodingChars(String message) {
224            return new EncodingCharacters(message.charAt(3), message.substring(4, 8));
225        }
226    
227    
228        /**
229         * Parses a message string and returns the corresponding Message object.
230         * Unexpected segments added at the end of their group.
231         * 
232         * @throws HL7Exception
233         *             if the message is not correctly formatted.
234         * @throws EncodingNotSupportedException
235         *             if the message encoded is not supported by this parser.
236         */
237        protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException {
238    
239            // try to instantiate a message object of the right class
240            MessageStructure structure = getStructure(message);
241            Message m = instantiateMessage(structure.messageStructure, version, structure.explicitlyDefined);
242    
243            parse(m, message);
244    
245            return m;
246        }
247    
248    
249        /**
250         * Generates (or returns the cached value of) the message
251         */
252        private IStructureDefinition getStructureDefinition(Class<? extends Message> theClazz) throws HL7Exception {
253    
254            StructureDefinition retVal = myStructureDefinitions.get(theClazz);
255            if (retVal != null) {
256                return retVal;
257            }
258    
259            Message message = ReflectionUtil.instantiateMessage(theClazz, getFactory());
260            Holder<StructureDefinition> previousLeaf = new Holder<StructureDefinition>();
261            retVal = createStructureDefinition(message, previousLeaf);
262            myStructureDefinitions.put(theClazz, retVal);
263    
264            return retVal;
265        }
266    
267    
268        private StructureDefinition createStructureDefinition(Structure theStructure, Holder<StructureDefinition> thePreviousLeaf) throws HL7Exception {
269    
270            StructureDefinition retVal = new StructureDefinition();
271            retVal.setName(theStructure.getName());
272    
273            if (theStructure instanceof Group) {
274                retVal.setSegment(false);
275                Group group = (Group) theStructure;
276                int index = 0;
277                for (String nextName : group.getNames()) {
278                    Structure nextChild = group.get(nextName);
279                    StructureDefinition structureDefinition = createStructureDefinition(nextChild, thePreviousLeaf);
280                    structureDefinition.setNameAsItAppearsInParent(nextName);
281                    structureDefinition.setRepeating(group.isRepeating(nextName));
282                    structureDefinition.setRequired(group.isRequired(nextName));
283                    structureDefinition.setPosition(index++);
284                    structureDefinition.setParent(retVal);
285                    retVal.addChild(structureDefinition);
286                }
287            } else {
288                if (thePreviousLeaf.getObject() != null) {
289                    thePreviousLeaf.getObject().setNextLeaf(retVal);
290                }
291                thePreviousLeaf.setObject(retVal);
292                retVal.setSegment(true);
293            }
294    
295            return retVal;
296        }
297    
298    
299        /**
300         * Parses a segment string and populates the given Segment object.
301         * Unexpected fields are added as Varies' at the end of the segment.
302         * 
303         * @param theRepetition
304         *            The repetition number of this segment within its group
305         * @throws HL7Exception
306         *             if the given string does not contain the given segment or if
307         *             the string is not encoded properly
308         */
309        public void parse(Segment destination, String segment, EncodingCharacters encodingChars) throws HL7Exception {
310            parse(destination, segment, encodingChars, null);
311        }
312    
313    
314        /**
315         * Parses a segment string and populates the given Segment object.
316         * Unexpected fields are added as Varies' at the end of the segment.
317         * 
318         * @param theRepetition
319         *            The repetition number of this segment within its group
320         * @throws HL7Exception
321         *             if the given string does not contain the given segment or if
322         *             the string is not encoded properly
323         */
324        public void parse(Segment destination, String segment, EncodingCharacters encodingChars, Integer theRepetition) throws HL7Exception {
325            int fieldOffset = 0;
326            if (isDelimDefSegment(destination.getName())) {
327                fieldOffset = 1;
328                // set field 1 to fourth character of string
329                Terser.set(destination, 1, 0, 1, 1, String.valueOf(encodingChars.getFieldSeparator()));
330            }
331    
332            String[] fields = split(segment, String.valueOf(encodingChars.getFieldSeparator()));
333            // destination.setName(fields[0]);
334            for (int i = 1; i < fields.length; i++) {
335                String[] reps = split(fields[i], String.valueOf(encodingChars.getRepetitionSeparator()));
336                if (log.isDebugEnabled()) {
337                    log.debug(reps.length + "reps delimited by: " + encodingChars.getRepetitionSeparator());
338                }
339    
340                // MSH-2 will get split incorrectly so we have to fudge it ...
341                boolean isMSH2 = isDelimDefSegment(destination.getName()) && i + fieldOffset == 2;
342                if (isMSH2) {
343                    reps = new String[1];
344                    reps[0] = fields[i];
345                }
346    
347                for (int j = 0; j < reps.length; j++) {
348                    try {
349                        if (log.isDebugEnabled()) {
350                            StringBuffer statusMessage = new StringBuffer("Parsing field ");
351                            statusMessage.append(i + fieldOffset);
352                            statusMessage.append(" repetition ");
353                            statusMessage.append(j);
354                            log.debug(statusMessage.toString());
355                        }
356                        // parse(destination.getField(i + fieldOffset, j), reps[j],
357                        // encodingChars, false);
358    
359                        Type field = destination.getField(i + fieldOffset, j);
360                        if (isMSH2) {
361                            Terser.getPrimitive(field, 1, 1).setValue(reps[j]);
362                        } else {
363                            parse(field, reps[j], encodingChars);
364                        }
365                    } catch (HL7Exception e) {
366                        // set the field location and throw again ...
367                        e.setFieldPosition(i);
368                        if (theRepetition != null) {
369                            e.setSegmentRepetition(theRepetition);
370                        }
371                        e.setSegmentName(destination.getName());
372                        throw e;
373                    }
374                }
375            }
376    
377            // set data type of OBX-5
378            if (destination.getClass().getName().indexOf("OBX") >= 0) {
379                Varies.fixOBX5(destination, getFactory());
380            }
381    
382        }
383    
384    
385        /**
386         * @return true if the segment is MSH, FHS, or BHS. These need special
387         *         treatment because they define delimiters.
388         * @param theSegmentName
389         */
390        private static boolean isDelimDefSegment(String theSegmentName) {
391            boolean is = false;
392            if (theSegmentName.equals("MSH") || theSegmentName.equals("FHS") || theSegmentName.equals("BHS")) {
393                is = true;
394            }
395            return is;
396        }
397    
398    
399        /**
400         * Fills a field with values from an unparsed string representing the field.
401         * 
402         * @param destinationField
403         *            the field Type
404         * @param data
405         *            the field string (including all components and subcomponents;
406         *            not including field delimiters)
407         * @param encodingCharacters
408         *            the encoding characters used in the message
409         */
410        @Override
411        public void parse(Type destinationField, String data, EncodingCharacters encodingCharacters) throws HL7Exception {
412            String[] components = split(data, String.valueOf(encodingCharacters.getComponentSeparator()));
413            for (int i = 0; i < components.length; i++) {
414                String[] subcomponents = split(components[i], String.valueOf(encodingCharacters.getSubcomponentSeparator()));
415                for (int j = 0; j < subcomponents.length; j++) {
416                    String val = subcomponents[j];
417                    if (val != null) {
418                        val = Escape.unescape(val, encodingCharacters);
419                    }
420                    Terser.getPrimitive(destinationField, i + 1, j + 1).setValue(val);
421                }
422            }
423        }
424    
425    
426        /**
427         * Splits the given composite string into an array of components using the
428         * given delimiter.
429         */
430        public static String[] split(String composite, String delim) {
431            ArrayList<String> components = new ArrayList<String>();
432    
433            // defend against evil nulls
434            if (composite == null)
435                composite = "";
436            if (delim == null)
437                delim = "";
438    
439            StringTokenizer tok = new StringTokenizer(composite, delim, true);
440            boolean previousTokenWasDelim = true;
441            while (tok.hasMoreTokens()) {
442                String thisTok = tok.nextToken();
443                if (thisTok.equals(delim)) {
444                    if (previousTokenWasDelim)
445                        components.add(null);
446                    previousTokenWasDelim = true;
447                } else {
448                    components.add(thisTok);
449                    previousTokenWasDelim = false;
450                }
451            }
452    
453            String[] ret = new String[components.size()];
454            for (int i = 0; i < components.size(); i++) {
455                ret[i] = (String) components.get(i);
456            }
457    
458            return ret;
459        }
460    
461    
462        /**
463         * {@inheritDoc }
464         */
465        @Override
466        public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception {
467            return encode(structure, encodingCharacters);
468        }
469    
470    
471        /**
472         * {@inheritDoc }
473         */
474        @Override
475        public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception {
476            return encode(type, encodingCharacters);
477        }
478    
479    
480        /**
481         * Encodes the given Type, using the given encoding characters. It is
482         * assumed that the Type represents a complete field rather than a
483         * component.
484         */
485        public static String encode(Type source, EncodingCharacters encodingChars) {
486            StringBuffer field = new StringBuffer();
487            for (int i = 1; i <= Terser.numComponents(source); i++) {
488                StringBuffer comp = new StringBuffer();
489                for (int j = 1; j <= Terser.numSubComponents(source, i); j++) {
490                    Primitive p = Terser.getPrimitive(source, i, j);
491                    comp.append(encodePrimitive(p, encodingChars));
492                    comp.append(encodingChars.getSubcomponentSeparator());
493                }
494                field.append(stripExtraDelimiters(comp.toString(), encodingChars.getSubcomponentSeparator()));
495                field.append(encodingChars.getComponentSeparator());
496            }
497            return stripExtraDelimiters(field.toString(), encodingChars.getComponentSeparator());
498            // return encode(source, encodingChars, false);
499        }
500    
501    
502        private static String encodePrimitive(Primitive p, EncodingCharacters encodingChars) {
503            String val = (p).getValue();
504            if (val == null) {
505                val = "";
506            } else {
507                val = Escape.escape(val, encodingChars);
508            }
509            return val;
510        }
511    
512    
513        /**
514         * Removes unecessary delimiters from the end of a field or segment. This
515         * seems to be more convenient than checking to see if they are needed while
516         * we are building the encoded string.
517         */
518        private static String stripExtraDelimiters(String in, char delim) {
519            char[] chars = in.toCharArray();
520    
521            // search from back end for first occurance of non-delimiter ...
522            int c = chars.length - 1;
523            boolean found = false;
524            while (c >= 0 && !found) {
525                if (chars[c--] != delim)
526                    found = true;
527            }
528    
529            String ret = "";
530            if (found)
531                ret = String.valueOf(chars, 0, c + 2);
532            return ret;
533        }
534    
535    
536        /**
537         * Formats a Message object into an HL7 message string using the given
538         * encoding.
539         * 
540         * @throws HL7Exception
541         *             if the data fields in the message do not permit encoding
542         *             (e.g. required fields are null)
543         * @throws EncodingNotSupportedException
544         *             if the requested encoding is not supported by this parser.
545         */
546        protected String doEncode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException {
547            if (!this.supportsEncoding(encoding))
548                throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding");
549    
550            return encode(source);
551        }
552    
553    
554        /**
555         * Formats a Message object into an HL7 message string using this parser's
556         * default encoding ("VB").
557         * 
558         * @throws HL7Exception
559         *             if the data fields in the message do not permit encoding
560         *             (e.g. required fields are null)
561         */
562        protected String doEncode(Message source) throws HL7Exception {
563            // get encoding characters ...
564            Segment msh = (Segment) source.get("MSH");
565            String fieldSepString = Terser.get(msh, 1, 0, 1, 1);
566    
567            if (fieldSepString == null)
568                throw new HL7Exception("Can't encode message: MSH-1 (field separator) is missing");
569    
570            char fieldSep = '|';
571            if (fieldSepString != null && fieldSepString.length() > 0)
572                fieldSep = fieldSepString.charAt(0);
573    
574            String encCharString = Terser.get(msh, 2, 0, 1, 1);
575    
576            if (encCharString == null)
577                throw new HL7Exception("Can't encode message: MSH-2 (encoding characters) is missing");
578    
579            if (encCharString.length() != 4)
580                throw new HL7Exception("Encoding characters '" + encCharString + "' invalid -- must be 4 characters", HL7Exception.DATA_TYPE_ERROR);
581            EncodingCharacters en = new EncodingCharacters(fieldSep, encCharString);
582    
583            // pass down to group encoding method which will operate recursively on
584            // children ...
585            return encode((Group) source, en);
586        }
587    
588    
589        /**
590         * Returns given group serialized as a pipe-encoded string - this method is
591         * called by encode(Message source, String encoding).
592         */
593        public static String encode(Group source, EncodingCharacters encodingChars) throws HL7Exception {
594            StringBuffer result = new StringBuffer();
595    
596            String[] names = source.getNames();
597            for (int i = 0; i < names.length; i++) {
598                Structure[] reps = source.getAll(names[i]);
599                for (int rep = 0; rep < reps.length; rep++) {
600                    if (reps[rep] instanceof Group) {
601                        result.append(encode((Group) reps[rep], encodingChars));
602                    } else {
603                        String segString = encode((Segment) reps[rep], encodingChars);
604                        if (segString.length() >= 4) {
605                            result.append(segString);
606                            result.append('\r');
607                        }
608                    }
609                }
610            }
611            return result.toString();
612        }
613    
614    
615        public static String encode(Segment source, EncodingCharacters encodingChars) {
616            StringBuffer result = new StringBuffer();
617            result.append(source.getName());
618            result.append(encodingChars.getFieldSeparator());
619    
620            // start at field 2 for MSH segment because field 1 is the field
621            // delimiter
622            int startAt = 1;
623            if (isDelimDefSegment(source.getName()))
624                startAt = 2;
625    
626            // loop through fields; for every field delimit any repetitions and add
627            // field delimiter after ...
628            int numFields = source.numFields();
629            for (int i = startAt; i <= numFields; i++) {
630                try {
631                    Type[] reps = source.getField(i);
632                    for (int j = 0; j < reps.length; j++) {
633                        String fieldText = encode(reps[j], encodingChars);
634                        // if this is MSH-2, then it shouldn't be escaped, so
635                        // unescape it again
636                        if (isDelimDefSegment(source.getName()) && i == 2)
637                            fieldText = Escape.unescape(fieldText, encodingChars);
638                        result.append(fieldText);
639                        if (j < reps.length - 1)
640                            result.append(encodingChars.getRepetitionSeparator());
641                    }
642                } catch (HL7Exception e) {
643                    log.error("Error while encoding segment: ", e);
644                }
645                result.append(encodingChars.getFieldSeparator());
646            }
647    
648            // strip trailing delimiters ...
649            return stripExtraDelimiters(result.toString(), encodingChars.getFieldSeparator());
650        }
651    
652    
653        /**
654         * Removes leading whitespace from the given string. This method was created
655         * to deal with frequent problems parsing messages that have been
656         * hand-written in windows. The intuitive way to delimit segments is to hit
657         * <ENTER> at the end of each segment, but this creates both a carriage
658         * return and a line feed, so to the parser, the first character of the next
659         * segment is the line feed.
660         */
661        public static String stripLeadingWhitespace(String in) {
662            StringBuffer out = new StringBuffer();
663            char[] chars = in.toCharArray();
664            int c = 0;
665            while (c < chars.length) {
666                if (!Character.isWhitespace(chars[c]))
667                    break;
668                c++;
669            }
670            for (int i = c; i < chars.length; i++) {
671                out.append(chars[i]);
672            }
673            return out.toString();
674        }
675    
676    
677        /**
678         * <p>
679         * Returns a minimal amount of data from a message string, including only
680         * the data needed to send a response to the remote system. This includes
681         * the following fields:
682         * <ul>
683         * <li>field separator</li>
684         * <li>encoding characters</li>
685         * <li>processing ID</li>
686         * <li>message control ID</li>
687         * </ul>
688         * This method is intended for use when there is an error parsing a message,
689         * (so the Message object is unavailable) but an error message must be sent
690         * back to the remote system including some of the information in the
691         * inbound message. This method parses only that required information,
692         * hopefully avoiding the condition that caused the original error. The
693         * other fields in the returned MSH segment are empty.
694         * </p>
695         */
696        public Segment getCriticalResponseData(String message) throws HL7Exception {
697            // try to get MSH segment
698            int locStartMSH = message.indexOf("MSH");
699            if (locStartMSH < 0)
700                throw new HL7Exception("Couldn't find MSH segment in message: " + message, HL7Exception.SEGMENT_SEQUENCE_ERROR);
701            int locEndMSH = message.indexOf('\r', locStartMSH + 1);
702            if (locEndMSH < 0)
703                locEndMSH = message.length();
704            String mshString = message.substring(locStartMSH, locEndMSH);
705    
706            // find out what the field separator is
707            char fieldSep = mshString.charAt(3);
708    
709            // get field array
710            String[] fields = split(mshString, String.valueOf(fieldSep));
711    
712            Segment msh = null;
713            try {
714                // parse required fields
715                String encChars = fields[1];
716                char compSep = encChars.charAt(0);
717                String messControlID = fields[9];
718                String[] procIDComps = split(fields[10], String.valueOf(compSep));
719    
720                // fill MSH segment
721                String version = "2.4"; // default
722                try {
723                    version = this.getVersion(message);
724                } catch (Exception e) { /* use the default */
725                }
726    
727                msh = Parser.makeControlMSH(version, getFactory());
728                Terser.set(msh, 1, 0, 1, 1, String.valueOf(fieldSep));
729                Terser.set(msh, 2, 0, 1, 1, encChars);
730                Terser.set(msh, 10, 0, 1, 1, messControlID);
731                Terser.set(msh, 11, 0, 1, 1, procIDComps[0]);
732                Terser.set(msh, 12, 0, 1, 1, version);
733    
734            } catch (Exception e) {
735                throw new HL7Exception("Can't parse critical fields from MSH segment (" + e.getClass().getName() + ": " + e.getMessage() + "): " + mshString, HL7Exception.REQUIRED_FIELD_MISSING, e);
736            }
737    
738            return msh;
739        }
740    
741    
742        /**
743         * For response messages, returns the value of MSA-2 (the message ID of the
744         * message sent by the sending system). This value may be needed prior to
745         * main message parsing, so that (particularly in a multi-threaded scenario)
746         * the message can be routed to the thread that sent the request. We need
747         * this information first so that any parse exceptions are thrown to the
748         * correct thread. Returns null if MSA-2 can not be found (e.g. if the
749         * message is not a response message).
750         */
751        public String getAckID(String message) {
752            String ackID = null;
753            int startMSA = message.indexOf("\rMSA");
754            if (startMSA >= 0) {
755                int startFieldOne = startMSA + 5;
756                char fieldDelim = message.charAt(startFieldOne - 1);
757                int start = message.indexOf(fieldDelim, startFieldOne) + 1;
758                int end = message.indexOf(fieldDelim, start);
759                int segEnd = message.indexOf(String.valueOf(segDelim), start);
760                if (segEnd > start && segEnd < end)
761                    end = segEnd;
762    
763                // if there is no field delim after MSH-2, need to go to end of
764                // message, but not including end seg delim if it exists
765                if (end < 0) {
766                    if (message.charAt(message.length() - 1) == '\r') {
767                        end = message.length() - 1;
768                    } else {
769                        end = message.length();
770                    }
771                }
772                if (start > 0 && end > start) {
773                    ackID = message.substring(start, end);
774                }
775            }
776            log.debug("ACK ID: " + ackID);
777            return ackID;
778        }
779    
780    
781        /**
782         * Defaults to <code>false</code>
783         * 
784         * @see #isLegacyMode()
785         */
786        public void setLegacyMode(boolean legacyMode) {
787            this.myLegacyMode = legacyMode;
788        }
789    
790    
791        /**
792         * {@inheritDoc }
793         */
794        @Override
795        public String encode(Message source) throws HL7Exception {
796            if (myLegacyMode != null && myLegacyMode) {
797                
798                @SuppressWarnings("deprecation")
799                OldPipeParser oldPipeParser = new OldPipeParser(getFactory());
800                
801                return oldPipeParser.encode(source);
802            }
803            return super.encode(source);
804        }
805    
806    
807        /**
808         * {@inheritDoc }
809         */
810        @Override
811        public Message parse(String message) throws HL7Exception, EncodingNotSupportedException {
812            if (myLegacyMode != null && myLegacyMode) {
813                
814                @SuppressWarnings("deprecation")
815                OldPipeParser oldPipeParser = new OldPipeParser(getFactory());
816                
817                return oldPipeParser.parse(message);
818            }
819            return super.parse(message);
820        }
821    
822    
823        /**
824         * <p>
825         * Returns <code>true</code> if legacy mode is on.
826         * </p>
827         * <p>
828         * Prior to release 1.0, when an unexpected segment was encountered in a
829         * message, HAPI would recurse to the deepest nesting in the last group it
830         * encountered after the current position in the message, and deposit the
831         * segment there. This could lead to unusual behaviour where all segments
832         * afterward would not be in an expected spot within the message.
833         * </p>
834         * <p>
835         * This should normally be set to false, but any code written before the
836         * release of HAPI 1.0 which depended on this behaviour might need legacy
837         * mode to be set to true.
838         * </p>
839         * <p>
840         * Defaults to <code>false</code>. Note that this method only overrides
841         * behaviour of the {@link #parse(java.lang.String)} and
842         * {@link #encode(ca.uhn.hl7v2.model.Message) } methods
843         * </p>
844         */
845        public boolean isLegacyMode() {
846            if (myLegacyMode == null) {
847                if (Boolean.parseBoolean(System.getProperty(DEFAULT_LEGACY_MODE_PROPERTY))) {
848                    return true;
849                } else {
850                    return false;
851                }
852            }
853            return this.myLegacyMode;
854        }
855    
856    
857        /**
858         * Returns the version ID (MSH-12) from the given message, without fully
859         * parsing the message. The version is needed prior to parsing in order to
860         * determine the message class into which the text of the message should be
861         * parsed.
862         * 
863         * @throws HL7Exception
864         *             if the version field can not be found.
865         */
866        public String getVersion(String message) throws HL7Exception {
867            int startMSH = message.indexOf("MSH");
868            int endMSH = message.indexOf(PipeParser.segDelim, startMSH);
869            if (endMSH < 0)
870                endMSH = message.length();
871            String msh = message.substring(startMSH, endMSH);
872            String fieldSep = null;
873            if (msh.length() > 3) {
874                fieldSep = String.valueOf(msh.charAt(3));
875            } else {
876                throw new HL7Exception("Can't find field separator in MSH: " + msh, HL7Exception.UNSUPPORTED_VERSION_ID);
877            }
878    
879            String[] fields = split(msh, fieldSep);
880    
881            String compSep = null;
882            if (fields.length >= 2 && fields[1] != null && fields[1].length() == 4) {
883                compSep = String.valueOf(fields[1].charAt(0)); // get component
884                                                               // separator as 1st
885                                                               // encoding char
886            } else {
887                throw new HL7Exception("Invalid or incomplete encoding characters - MSH-2 is " + fields[1], HL7Exception.REQUIRED_FIELD_MISSING);
888            }
889    
890            String version = null;
891            if (fields.length >= 12) {
892                String[] comp = split(fields[11], compSep);
893                if (comp.length >= 1) {
894                    version = comp[0];
895                } else {
896                    throw new HL7Exception("Can't find version ID - MSH.12 is " + fields[11], HL7Exception.REQUIRED_FIELD_MISSING);
897                }
898            } else {
899                throw new HL7Exception("Can't find version ID - MSH has only " + fields.length + " fields.", HL7Exception.REQUIRED_FIELD_MISSING);
900            }
901            return version;
902        }
903    
904    
905        @Override
906        public void parse(Message message, String string) throws HL7Exception {
907            IStructureDefinition structureDef = getStructureDefinition(message.getClass());
908    
909            // MessagePointer ptr = new MessagePointer(this, m,
910            // getEncodingChars(message));
911            MessageIterator messageIter = new MessageIterator(message, structureDef, "MSH", true);
912    
913            String[] segments = split(string, segDelim);
914    
915            char delim = '|';
916            for (int i = 0; i < segments.length; i++) {
917    
918                // get rid of any leading whitespace characters ...
919                if (segments[i] != null && segments[i].length() > 0 && Character.isWhitespace(segments[i].charAt(0)))
920                    segments[i] = stripLeadingWhitespace(segments[i]);
921    
922                // sometimes people put extra segment delimiters at end of msg ...
923                if (segments[i] != null && segments[i].length() >= 3) {
924                    final String name;
925                    if (i == 0) {
926                        name = segments[i].substring(0, 3);
927                        delim = segments[i].charAt(3);
928                    } else {
929                        if (segments[i].indexOf(delim) >= 0) {
930                            name = segments[i].substring(0, segments[i].indexOf(delim));
931                        } else {
932                            name = segments[i];
933                        }
934                    }
935    
936                    log.debug("Parsing segment " + name);
937    
938                    messageIter.setDirection(name);
939    
940                    if (messageIter.hasNext()) {
941                        Segment next = (Segment) messageIter.next();
942                        int nextIndexWithinParent = messageIter.getNextIndexWithinParent();
943                        parse(next, segments[i], getEncodingChars(string), nextIndexWithinParent);
944                    }
945                }
946            }
947        }
948    
949        /**
950         * A struct for holding a message class string and a boolean indicating
951         * whether it was defined explicitly.
952         */
953        private static class MessageStructure {
954            public String messageStructure;
955            public boolean explicitlyDefined;
956    
957    
958            public MessageStructure(String theMessageStructure, boolean isExplicitlyDefined) {
959                messageStructure = theMessageStructure;
960                explicitlyDefined = isExplicitlyDefined;
961            }
962        }
963    
964        private static class Holder<T> {
965            private T myObject;
966    
967    
968            public T getObject() {
969                return myObject;
970            }
971    
972    
973            public void setObject(T theObject) {
974                myObject = theObject;
975            }
976        }
977    
978    }