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.HashMap;
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.DoNotCacheStructure;
039    import ca.uhn.hl7v2.model.Group;
040    import ca.uhn.hl7v2.model.Message;
041    import ca.uhn.hl7v2.model.Primitive;
042    import ca.uhn.hl7v2.model.Segment;
043    import ca.uhn.hl7v2.model.Structure;
044    import ca.uhn.hl7v2.model.Type;
045    import ca.uhn.hl7v2.model.Varies;
046    import ca.uhn.hl7v2.util.ReflectionUtil;
047    import ca.uhn.hl7v2.util.Terser;
048    import ca.uhn.hl7v2.validation.impl.NoValidation;
049    import ca.uhn.hl7v2.validation.impl.ValidationContextFactory;
050    
051    /**
052     * An implementation of Parser that supports traditionally encoded (ie delimited
053     * with characters like |, ^, and ~) HL7 messages. Unexpected segments and
054     * fields are parsed into generic elements that are added to the message.
055     * 
056     * @author Bryan Tripp (bryan_tripp@sourceforge.net)
057     */
058    public class PipeParser extends Parser {
059    
060        private static final Logger log = LoggerFactory.getLogger(PipeParser.class);
061    
062        /**
063         * The HL7 ER7 segment delimiter (see section 2.8 of spec)
064         */
065        final static String SEGMENT_DELIMITER = "\r";
066    
067        private final HashMap<Class<? extends Message>, StructureDefinition> myStructureDefinitions = new HashMap<Class<? extends Message>, StructureDefinition>();
068    
069        /**
070         * System property key. If value is "true", legacy mode will default to true
071         * 
072         * @see #isLegacyMode()
073         * @deprecated This will be removed in HAPI 3.0
074         */
075        public static final String DEFAULT_LEGACY_MODE_PROPERTY = "ca.uhn.hl7v2.parser.PipeParser.default_legacy_mode";
076    
077        private Boolean myLegacyMode = null;
078    
079        
080            /** Creates a new PipeParser */
081        public PipeParser() {
082            this(null);
083        }
084    
085    
086        /**
087         * Creates a new PipeParser
088         * 
089         * @param theFactory
090         *            custom factory to use for model class lookup
091         */
092        public PipeParser(ModelClassFactory theFactory) {
093            super(theFactory);
094        }
095    
096    
097        /**
098         * Returns a String representing the encoding of the given message, if the
099         * encoding is recognized. For example if the given message appears to be
100         * encoded using HL7 2.x XML rules then "XML" would be returned. If the
101         * encoding is not recognized then null is returned. That this method
102         * returns a specific encoding does not guarantee that the message is
103         * correctly encoded (e.g. well formed XML) - just that it is not encoded
104         * using any other encoding than the one returned.
105         */
106        public String getEncoding(String message) {
107            if (EncodingDetector.isEr7Encoded(message)) {
108                return "VB";
109            }
110            return null;
111        }
112    
113    
114            /**
115         * @return the preferred encoding of this Parser
116         */
117        public String getDefaultEncoding() {
118            return "VB";
119        }
120    
121    
122        /**
123         * Returns true if and only if the given encoding is supported by this
124         * Parser.
125         */
126        public boolean supportsEncoding(String encoding) {
127            boolean supports = false;
128            if (encoding != null && encoding.equals("VB"))
129                supports = true;
130            return supports;
131        }
132    
133    
134        /**
135         * @deprecated this method should not be public
136         * @param message
137         * @return
138         * @throws HL7Exception
139         * @throws EncodingNotSupportedException
140         */
141        public String getMessageStructure(String message) throws HL7Exception, EncodingNotSupportedException {
142            return getStructure(message).messageStructure;
143        }
144    
145    
146        /**
147         * @returns the message structure from MSH-9-3
148         */
149        private MessageStructure getStructure(String message) throws HL7Exception, EncodingNotSupportedException {
150            EncodingCharacters ec = getEncodingChars(message);
151            String messageStructure = null;
152            boolean explicityDefined = true;
153            String wholeFieldNine;
154            try {
155                String[] fields = split(message.substring(0, Math.max(message.indexOf(SEGMENT_DELIMITER), message.length())), String.valueOf(ec.getFieldSeparator()));
156                wholeFieldNine = fields[8];
157    
158                // message structure is component 3 but we'll accept a composite of
159                // 1 and 2 if there is no component 3 ...
160                // if component 1 is ACK, then the structure is ACK regardless of
161                // component 2
162                String[] comps = split(wholeFieldNine, String.valueOf(ec.getComponentSeparator()));
163                if (comps.length >= 3) {
164                    messageStructure = comps[2];
165                } else if (comps.length > 0 && comps[0] != null && comps[0].equals("ACK")) {
166                    messageStructure = "ACK";
167                } else if (comps.length == 2) {
168                    explicityDefined = false;
169                    messageStructure = comps[0] + "_" + comps[1];
170                }
171                /*
172                 * else if (comps.length == 1 && comps[0] != null &&
173                 * comps[0].equals("ACK")) { messageStructure = "ACK"; //it's common
174                 * for people to only populate component 1 in an ACK msg }
175                 */
176                else {
177                    StringBuilder buf = new StringBuilder("Can't determine message structure from MSH-9: ");
178                    buf.append(wholeFieldNine);
179                    if (comps.length < 3) {
180                        buf.append(" HINT: there are only ");
181                        buf.append(comps.length);
182                        buf.append(" of 3 components present");
183                    }
184                    throw new HL7Exception(buf.toString(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE);
185                }
186            } catch (IndexOutOfBoundsException e) {
187                throw new HL7Exception("Can't find message structure (MSH-9-3): " + e.getMessage(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE);
188            }
189    
190            return new MessageStructure(messageStructure, explicityDefined);
191        }
192    
193    
194        /**
195         * Returns object that contains the field separator and encoding characters
196         * for this message.
197         * @throws HL7Exception 
198         */
199        private static EncodingCharacters getEncodingChars(String message) throws HL7Exception {
200            if (message.length() < 8) {
201                    throw new HL7Exception("Invalid message content: \"" + message + "\"");
202            }
203            return new EncodingCharacters(message.charAt(3), message.substring(4, 8));
204        }
205    
206    
207        /**
208         * Parses a message string and returns the corresponding Message object.
209         * Unexpected segments added at the end of their group.
210         * 
211         * @throws HL7Exception
212         *             if the message is not correctly formatted.
213         * @throws EncodingNotSupportedException
214         *             if the message encoded is not supported by this parser.
215         */
216        protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException {
217    
218            // try to instantiate a message object of the right class
219            MessageStructure structure = getStructure(message);
220            Message m = instantiateMessage(structure.messageStructure, version, structure.explicitlyDefined);
221    
222            parse(m, message);
223    
224            return m;
225        }
226    
227        
228        /**
229         * {@inheritDoc}
230         */
231        protected Message doParseForSpecificPackage(String message, String version, String packageName) throws HL7Exception, EncodingNotSupportedException { 
232    
233            // try to instantiate a message object of the right class 
234            MessageStructure structure = getStructure(message); 
235            Message m = instantiateMessageInASpecificPackage(structure.messageStructure, version, structure.explicitlyDefined, packageName); 
236    
237            parse(m, message); 
238    
239            return m; 
240        }
241        
242    
243        /**
244         * Generates (or returns the cached value of) the message
245         */
246        private IStructureDefinition getStructureDefinition(Message theMessage) throws HL7Exception {
247    
248            Class<? extends Message> clazz = theMessage.getClass();
249            StructureDefinition retVal = myStructureDefinitions.get(clazz);
250            if (retVal != null) {
251                return retVal;
252            }
253            
254            if (clazz.isAnnotationPresent(DoNotCacheStructure.class)) {
255                Holder<StructureDefinition> previousLeaf = new Holder<StructureDefinition>();
256                retVal = createStructureDefinition(theMessage, previousLeaf);
257            } else {
258                    Message message = ReflectionUtil.instantiateMessage(clazz, getFactory());
259                    Holder<StructureDefinition> previousLeaf = new Holder<StructureDefinition>();
260                    retVal = createStructureDefinition(message, previousLeaf);
261                    myStructureDefinitions.put(clazz, retVal);
262            }
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    
337                // MSH-2 will get split incorrectly so we have to fudge it ...
338                boolean isMSH2 = isDelimDefSegment(destination.getName()) && i + fieldOffset == 2;
339                if (isMSH2) {
340                    reps = new String[1];
341                    reps[0] = fields[i];
342                }
343    
344                for (int j = 0; j < reps.length; j++) {
345                    try {
346                            log.debug("Parsing field {} repetition {}", i + fieldOffset, j);
347                        Type field = destination.getField(i + fieldOffset, j);
348                        if (isMSH2) {
349                            Terser.getPrimitive(field, 1, 1).setValue(reps[j]);
350                        } else {
351                            parse(field, reps[j], encodingChars);
352                        }
353                    } catch (HL7Exception e) {
354                        // set the field location and throw again ...
355                        e.setFieldPosition(i);
356                        if (theRepetition != null && theRepetition > 1) {
357                            e.setSegmentRepetition(theRepetition);
358                        }
359                        e.setSegmentName(destination.getName());
360                        throw e;
361                    }
362                }
363            }
364    
365            // set data type of OBX-5
366            if (destination.getClass().getName().indexOf("OBX") >= 0) {
367                Varies.fixOBX5(destination, getFactory());
368            }
369    
370        }
371    
372    
373        /**
374         * @return true if the segment is MSH, FHS, or BHS. These need special
375         *         treatment because they define delimiters.
376         * @param theSegmentName
377         */
378        private static boolean isDelimDefSegment(String theSegmentName) {
379            boolean is = false;
380            if (theSegmentName.equals("MSH") || theSegmentName.equals("FHS") || theSegmentName.equals("BHS")) {
381                is = true;
382            }
383            return is;
384        }
385    
386    
387        /**
388         * Fills a field with values from an unparsed string representing the field.
389         * 
390         * @param destinationField
391         *            the field Type
392         * @param data
393         *            the field string (including all components and subcomponents;
394         *            not including field delimiters)
395         * @param encodingCharacters
396         *            the encoding characters used in the message
397         */
398        @Override
399        public void parse(Type destinationField, String data, EncodingCharacters encodingCharacters) throws HL7Exception {
400            String[] components = split(data, String.valueOf(encodingCharacters.getComponentSeparator()));
401            for (int i = 0; i < components.length; i++) {
402                String[] subcomponents = split(components[i], String.valueOf(encodingCharacters.getSubcomponentSeparator()));
403                for (int j = 0; j < subcomponents.length; j++) {
404                    String val = subcomponents[j];
405                    if (val != null) {
406                        val = Escape.unescape(val, encodingCharacters);
407                    }
408                    Terser.getPrimitive(destinationField, i + 1, j + 1).setValue(val);
409                }
410            }
411        }
412    
413    
414        /**
415         * Splits the given composite string into an array of components using the
416         * given delimiter.
417         */
418        public static String[] split(String composite, String delim) {
419            ArrayList<String> components = new ArrayList<String>();
420    
421            // defend against evil nulls
422            if (composite == null)
423                composite = "";
424            if (delim == null)
425                delim = "";
426    
427            StringTokenizer tok = new StringTokenizer(composite, delim, true);
428            boolean previousTokenWasDelim = true;
429            while (tok.hasMoreTokens()) {
430                String thisTok = tok.nextToken();
431                if (thisTok.equals(delim)) {
432                    if (previousTokenWasDelim)
433                        components.add(null);
434                    previousTokenWasDelim = true;
435                } else {
436                    components.add(thisTok);
437                    previousTokenWasDelim = false;
438                }
439            }
440    
441            String[] ret = new String[components.size()];
442            for (int i = 0; i < components.size(); i++) {
443                ret[i] = (String) components.get(i);
444            }
445    
446            return ret;
447        }
448    
449    
450        /**
451         * {@inheritDoc }
452         */
453        @Override
454        public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception {
455            return encode(structure, encodingCharacters);
456        }
457    
458    
459        /**
460         * {@inheritDoc }
461         */
462        @Override
463        public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception {
464            return encode(type, encodingCharacters);
465        }
466    
467    
468        /**
469         * Encodes the given Type, using the given encoding characters. It is
470         * assumed that the Type represents a complete field rather than a
471         * component.
472         */
473        public static String encode(Type source, EncodingCharacters encodingChars) {
474            return encode(source, encodingChars, null, null);
475        }
476    
477    
478        private static String encode(Type source, EncodingCharacters encodingChars, ParserConfiguration parserConfig, String currentTerserPath) {
479            if (source instanceof Varies) {
480                    Varies varies = (Varies) source;
481                    if (varies.getData() != null) {
482                            source = varies.getData();
483                    }
484            }
485            
486            StringBuilder field = new StringBuilder();
487            for (int i = 1; i <= Terser.numComponents(source); i++) {
488                StringBuilder comp = new StringBuilder();
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            
498            int forceUpToFieldNum = 0;
499            if (parserConfig != null && currentTerserPath != null) {
500                    for (String nextPath : parserConfig.getForcedEncode()) {
501                            if (nextPath.startsWith(currentTerserPath + "-") && nextPath.length() > currentTerserPath.length()) {
502                                    int endOfFieldDef = nextPath.indexOf('-', currentTerserPath.length());
503                                    if (endOfFieldDef == -1) {
504                                            forceUpToFieldNum = 0;
505                                            break;
506                                    }
507                                    String fieldNumString = nextPath.substring(endOfFieldDef + 1, nextPath.length());
508                                    if (fieldNumString.length() > 0) {
509                                            forceUpToFieldNum = Math.max(forceUpToFieldNum, Integer.parseInt(fieldNumString));
510                                    }
511                            }
512                    }
513            }
514            
515            char componentSeparator = encodingChars.getComponentSeparator();
516                    String retVal = stripExtraDelimiters(field.toString(), componentSeparator);
517            
518                    while (forceUpToFieldNum > 0 && (countInstancesOf(retVal, componentSeparator) + 1) < forceUpToFieldNum) {
519                            retVal = retVal + componentSeparator;
520                    }
521            
522                    return retVal;
523            }
524    
525    
526            private static String encodePrimitive(Primitive p, EncodingCharacters encodingChars) {
527            String val = (p).getValue();
528            if (val == null) {
529                val = "";
530            } else {
531                val = Escape.escape(val, encodingChars);
532            }
533            return val;
534        }
535    
536    
537        /**
538         * Removes unecessary delimiters from the end of a field or segment. This
539         * seems to be more convenient than checking to see if they are needed while
540         * we are building the encoded string.
541         */
542        private static String stripExtraDelimiters(String in, char delim) {
543            char[] chars = in.toCharArray();
544    
545            // search from back end for first occurance of non-delimiter ...
546            int c = chars.length - 1;
547            boolean found = false;
548            while (c >= 0 && !found) {
549                if (chars[c--] != delim)
550                    found = true;
551            }
552    
553            String ret = "";
554            if (found)
555                ret = String.valueOf(chars, 0, c + 2);
556            return ret;
557        }
558    
559    
560        /**
561         * Formats a Message object into an HL7 message string using the given
562         * encoding.
563         * 
564         * @throws HL7Exception
565         *             if the data fields in the message do not permit encoding
566         *             (e.g. required fields are null)
567         * @throws EncodingNotSupportedException
568         *             if the requested encoding is not supported by this parser.
569         */
570        protected String doEncode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException {
571            if (!this.supportsEncoding(encoding))
572                throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding");
573    
574            return encode(source);
575        }
576    
577    
578        /**
579         * Formats a Message object into an HL7 message string using this parser's
580         * default encoding ("VB").
581         * 
582         * @throws HL7Exception
583         *             if the data fields in the message do not permit encoding
584         *             (e.g. required fields are null)
585         */
586        protected String doEncode(Message source) throws HL7Exception {
587            // get encoding characters ...
588            Segment msh = (Segment) source.get("MSH");
589            String fieldSepString = Terser.get(msh, 1, 0, 1, 1);
590    
591            if (fieldSepString == null)
592                throw new HL7Exception("Can't encode message: MSH-1 (field separator) is missing");
593    
594            char fieldSep = '|';
595            if (fieldSepString != null && fieldSepString.length() > 0)
596                fieldSep = fieldSepString.charAt(0);
597    
598            String encCharString = Terser.get(msh, 2, 0, 1, 1);
599    
600            if (encCharString == null)
601                throw new HL7Exception("Can't encode message: MSH-2 (encoding characters) is missing");
602    
603            if (encCharString.length() != 4)
604                throw new HL7Exception("Encoding characters (MSH-2) value '" + encCharString + "' invalid -- must be 4 characters", HL7Exception.DATA_TYPE_ERROR);
605            EncodingCharacters en = new EncodingCharacters(fieldSep, encCharString);
606    
607            // pass down to group encoding method which will operate recursively on
608            // children ...
609            return encode((Group) source, en, getParserConfiguration(), "");
610        }
611    
612    
613        /**
614         * Returns given group serialized as a pipe-encoded string - this method is
615         * called by encode(Message source, String encoding).
616         */
617        public static String encode(Group source, EncodingCharacters encodingChars) throws HL7Exception {
618            return encode(source, encodingChars, source.getMessage().getParser().getParserConfiguration(), "");
619        }
620    
621    
622        /**
623         * Returns given group serialized as a pipe-encoded string - this method is
624         * called by encode(Message source, String encoding).
625         */
626            private static String encode(Group source, EncodingCharacters encodingChars, ParserConfiguration parserConfiguration, String currentTerserPath) throws HL7Exception {
627                    StringBuilder result = new StringBuilder();
628    
629            String[] names = source.getNames();
630            
631            String firstMandatorySegmentName = null;
632            boolean haveEncounteredMandatorySegment = false;
633            boolean haveEncounteredContent = false;
634            boolean haveHadMandatorySegment = false;
635            boolean haveHadSegmentBeforeMandatorySegment = false;
636                    
637            for (int i = 0; i < names.length; i++) {
638                    
639                String nextName = names[i];
640                source.get(nextName, 0);
641                            Structure[] reps = source.getAll(nextName);
642                boolean nextNameIsRequired = source.isRequired(nextName);
643            
644                boolean havePreviouslyEncounteredMandatorySegment = haveEncounteredMandatorySegment;
645                haveEncounteredMandatorySegment |= nextNameIsRequired;
646                if (nextNameIsRequired && !haveHadMandatorySegment) {
647                    if (!source.isGroup(nextName)) {
648                            firstMandatorySegmentName = nextName;
649                    }
650                }
651                
652                String nextTerserPath = currentTerserPath.length() > 0 ? currentTerserPath + "/" + nextName : nextName;
653                
654                // Add all reps of the next segment/group
655                for (int rep = 0; rep < reps.length; rep++) {
656                    
657                    if (reps[rep] instanceof Group) {
658                        
659                            String encodedGroup = encode((Group) reps[rep], encodingChars, parserConfiguration, nextTerserPath);
660                                            result.append(encodedGroup);
661                                            
662                                            if (encodedGroup.length() > 0) {
663                            if (!haveHadMandatorySegment && !haveEncounteredMandatorySegment) {
664                                    haveHadSegmentBeforeMandatorySegment = true;
665                            }
666                            if (nextNameIsRequired && !haveHadMandatorySegment && !havePreviouslyEncounteredMandatorySegment) {
667                                    haveHadMandatorySegment = true;
668                            }
669                                                    haveEncounteredContent = true;
670                                            }
671                                            
672                    } else {
673    
674                            // Check if we are configured to force the encoding of this segment
675                            boolean encodeEmptySegments = parserConfiguration.determineForcedEncodeIncludesTerserPath(nextTerserPath);
676                            String segString = encode((Segment) reps[rep], encodingChars, parserConfiguration, nextTerserPath);
677                        if (segString.length() >= 4 || encodeEmptySegments) {
678                            result.append(segString);
679                            
680                            if (segString.length() == 3) {
681                                    result.append(encodingChars.getFieldSeparator());
682                            }
683                            
684                            result.append(SEGMENT_DELIMITER);
685                            
686                            haveEncounteredContent = true;
687                            
688                            if (nextNameIsRequired) {
689                                    haveHadMandatorySegment = true;
690                            }
691                            
692                            if (!haveHadMandatorySegment && !haveEncounteredMandatorySegment) {
693                                    haveHadSegmentBeforeMandatorySegment = true;
694                            }
695                            
696                        } 
697                        
698                    }
699                    
700                }
701                            
702            }
703            
704            if (firstMandatorySegmentName != null && !haveHadMandatorySegment && 
705                            !haveHadSegmentBeforeMandatorySegment && haveEncounteredContent && 
706                            parserConfiguration.isEncodeEmptyMandatorySegments()) {
707                    return firstMandatorySegmentName.substring(0, 3) + encodingChars.getFieldSeparator() + SEGMENT_DELIMITER + result;
708            } else {
709                    return result.toString();
710            }
711            }
712    
713        /**
714         * Convenience factory method which returns an instance that has a 
715         * {@link NoValidation NoValidation validation context}. 
716         */
717        public static PipeParser getInstanceWithNoValidation() {
718            PipeParser retVal = new PipeParser();
719            retVal.setValidationContext(ValidationContextFactory.noValidation());
720            return retVal;
721        }
722    
723        public static String encode(Segment source, EncodingCharacters encodingChars) {
724            return encode(source, encodingChars, null, null);
725        }
726    
727    
728        private static String encode(Segment source, EncodingCharacters encodingChars, ParserConfiguration parserConfig, String currentTerserPath) {
729            StringBuilder result = new StringBuilder();
730            result.append(source.getName());
731            result.append(encodingChars.getFieldSeparator());
732    
733            // start at field 2 for MSH segment because field 1 is the field
734            // delimiter
735            int startAt = 1;
736            if (isDelimDefSegment(source.getName()))
737                startAt = 2;
738    
739            // loop through fields; for every field delimit any repetitions and add
740            // field delimiter after ...
741            int numFields = source.numFields();
742    
743            int forceUpToFieldNum = 0;
744            if (parserConfig != null && currentTerserPath != null) {
745                    forceUpToFieldNum = parserConfig.determineForcedFieldNumForTerserPath(currentTerserPath);
746            }
747            numFields = Math.max(numFields, forceUpToFieldNum);
748            
749            for (int i = startAt; i <= numFields; i++) {
750                    
751                    String nextFieldTerserPath = currentTerserPath + "-" + i;
752                if (parserConfig != null && currentTerserPath != null) {
753                    for (String nextPath : parserConfig.getForcedEncode()) {
754                            if (nextPath.startsWith(nextFieldTerserPath + "-")) {
755                                    try {
756                                                            source.getField(i, 0);
757                                                    } catch (HL7Exception e) {
758                                            log.error("Error while encoding segment: ", e);
759                                                    }
760                            }
761                    }
762                }
763                
764                try {
765                    Type[] reps = source.getField(i);
766                    for (int j = 0; j < reps.length; j++) {
767                        String fieldText = encode(reps[j], encodingChars, parserConfig, nextFieldTerserPath);
768                        // if this is MSH-2, then it shouldn't be escaped, so
769                        // unescape it again
770                        if (isDelimDefSegment(source.getName()) && i == 2)
771                            fieldText = Escape.unescape(fieldText, encodingChars);
772                        result.append(fieldText);
773                        if (j < reps.length - 1)
774                            result.append(encodingChars.getRepetitionSeparator());
775                    }
776                } catch (HL7Exception e) {
777                    log.error("Error while encoding segment: ", e);
778                }
779                result.append(encodingChars.getFieldSeparator());
780            }
781    
782            // strip trailing delimiters ...
783            char fieldSeparator = encodingChars.getFieldSeparator();
784                    String retVal = stripExtraDelimiters(result.toString(), fieldSeparator);
785                    
786                    int offset = isDelimDefSegment(source.getName()) ? 1 : 0;
787                    while (forceUpToFieldNum > 0 && (countInstancesOf(retVal, fieldSeparator) + offset) < forceUpToFieldNum) {
788                            retVal = retVal + fieldSeparator;
789                    }
790                    
791                    return retVal;
792            }
793    
794    
795            private static int countInstancesOf(String theString, char theCharToSearchFor) {
796                    int retVal = 0;
797                    for (int i = 0; i < theString.length(); i++) {
798                            if (theString.charAt(i) == theCharToSearchFor) {
799                                    retVal++;
800                            }
801                    }
802                    return retVal;
803            }
804    
805    
806            /**
807         * Removes leading whitespace from the given string. This method was created
808         * to deal with frequent problems parsing messages that have been
809         * hand-written in windows. The intuitive way to delimit segments is to hit
810         * <ENTER> at the end of each segment, but this creates both a carriage
811         * return and a line feed, so to the parser, the first character of the next
812         * segment is the line feed.
813         */
814        public static String stripLeadingWhitespace(String in) {
815            StringBuilder out = new StringBuilder();
816            char[] chars = in.toCharArray();
817            int c = 0;
818            while (c < chars.length) {
819                if (!Character.isWhitespace(chars[c]))
820                    break;
821                c++;
822            }
823            for (int i = c; i < chars.length; i++) {
824                out.append(chars[i]);
825            }
826            return out.toString();
827        }
828    
829    
830        /**
831         * <p>
832         * Returns a minimal amount of data from a message string, including only
833         * the data needed to send a response to the remote system. This includes
834         * the following fields:
835         * <ul>
836         * <li>field separator</li>
837         * <li>encoding characters</li>
838         * <li>processing ID</li>
839         * <li>message control ID</li>
840         * </ul>
841         * This method is intended for use when there is an error parsing a message,
842         * (so the Message object is unavailable) but an error message must be sent
843         * back to the remote system including some of the information in the
844         * inbound message. This method parses only that required information,
845         * hopefully avoiding the condition that caused the original error. The
846         * other fields in the returned MSH segment are empty.
847         * </p>
848         */
849        public Segment getCriticalResponseData(String message) throws HL7Exception {
850            // try to get MSH segment
851            int locStartMSH = message.indexOf("MSH");
852            if (locStartMSH < 0)
853                throw new HL7Exception("Couldn't find MSH segment in message: " + message, HL7Exception.SEGMENT_SEQUENCE_ERROR);
854            int locEndMSH = message.indexOf('\r', locStartMSH + 1);
855            if (locEndMSH < 0)
856                locEndMSH = message.length();
857            String mshString = message.substring(locStartMSH, locEndMSH);
858    
859            // find out what the field separator is
860            char fieldSep = mshString.charAt(3);
861    
862            // get field array
863            String[] fields = split(mshString, String.valueOf(fieldSep));
864    
865            Segment msh = null;
866            try {
867                // parse required fields
868                String encChars = fields[1];
869                char compSep = encChars.charAt(0);
870                String messControlID = fields[9];
871                String[] procIDComps = split(fields[10], String.valueOf(compSep));
872    
873                // fill MSH segment
874                String version = "2.4"; // default
875                try {
876                    version = this.getVersion(message);
877                } catch (Exception e) { /* use the default */
878                }
879    
880                msh = Parser.makeControlMSH(version, getFactory());
881                Terser.set(msh, 1, 0, 1, 1, String.valueOf(fieldSep));
882                Terser.set(msh, 2, 0, 1, 1, encChars);
883                Terser.set(msh, 10, 0, 1, 1, messControlID);
884                Terser.set(msh, 11, 0, 1, 1, procIDComps[0]);
885                Terser.set(msh, 12, 0, 1, 1, version);
886    
887            } catch (Exception e) {
888                throw new HL7Exception("Can't parse critical fields from MSH segment (" + e.getClass().getName() + ": " + e.getMessage() + "): " + mshString, HL7Exception.REQUIRED_FIELD_MISSING, e);
889            }
890    
891            return msh;
892        }
893    
894    
895        /**
896         * For response messages, returns the value of MSA-2 (the message ID of the
897         * message sent by the sending system). This value may be needed prior to
898         * main message parsing, so that (particularly in a multi-threaded scenario)
899         * the message can be routed to the thread that sent the request. We need
900         * this information first so that any parse exceptions are thrown to the
901         * correct thread. Returns null if MSA-2 can not be found (e.g. if the
902         * message is not a response message).
903         */
904        public String getAckID(String message) {
905            String ackID = null;
906            int startMSA = message.indexOf("\rMSA");
907            if (startMSA >= 0) {
908                int startFieldOne = startMSA + 5;
909                char fieldDelim = message.charAt(startFieldOne - 1);
910                int start = message.indexOf(fieldDelim, startFieldOne) + 1;
911                int end = message.indexOf(fieldDelim, start);
912                int segEnd = message.indexOf(String.valueOf(SEGMENT_DELIMITER), start);
913                if (segEnd > start && segEnd < end)
914                    end = segEnd;
915    
916                // if there is no field delim after MSH-2, need to go to end of
917                // message, but not including end seg delim if it exists
918                if (end < 0) {
919                    if (message.charAt(message.length() - 1) == '\r') {
920                        end = message.length() - 1;
921                    } else {
922                        end = message.length();
923                    }
924                }
925                if (start > 0 && end > start) {
926                    ackID = message.substring(start, end);
927                }
928            }
929            log.debug("ACK ID: {}", ackID);
930            return ackID;
931        }
932    
933    
934        /**
935         * Defaults to <code>false</code>
936         * 
937         * @see #isLegacyMode()
938         * @deprecated This will be removed in HAPI 3.0
939         */
940        public void setLegacyMode(boolean legacyMode) {
941            this.myLegacyMode = legacyMode;
942        }
943    
944    
945        /**
946         * {@inheritDoc }
947         */
948        @Override
949        public String encode(Message source) throws HL7Exception {
950            if (myLegacyMode != null && myLegacyMode) {
951                
952                @SuppressWarnings("deprecation")
953                OldPipeParser oldPipeParser = new OldPipeParser(getFactory());
954                
955                return oldPipeParser.encode(source);
956            }
957            return super.encode(source);
958        }
959    
960    
961        /**
962         * {@inheritDoc }
963         */
964        @Override
965        public Message parse(String message) throws HL7Exception, EncodingNotSupportedException {
966            if (myLegacyMode != null && myLegacyMode) {
967                
968                @SuppressWarnings("deprecation")
969                OldPipeParser oldPipeParser = new OldPipeParser(getFactory());
970                
971                return oldPipeParser.parse(message);
972            }
973            return super.parse(message);
974        }
975    
976    
977        /**
978         * <p>
979         * Returns <code>true</code> if legacy mode is on.
980         * </p>
981         * <p>
982         * Prior to release 1.0, when an unexpected segment was encountered in a
983         * message, HAPI would recurse to the deepest nesting in the last group it
984         * encountered after the current position in the message, and deposit the
985         * segment there. This could lead to unusual behaviour where all segments
986         * afterward would not be in an expected spot within the message.
987         * </p>
988         * <p>
989         * This should normally be set to false, but any code written before the
990         * release of HAPI 1.0 which depended on this behaviour might need legacy
991         * mode to be set to true.
992         * </p>
993         * <p>
994         * Defaults to <code>false</code>. Note that this method only overrides
995         * behaviour of the {@link #parse(java.lang.String)} and
996         * {@link #encode(ca.uhn.hl7v2.model.Message) } methods
997         * </p>
998         * 
999         * @deprecated This will be removed in HAPI 3.0
1000         */
1001        public boolean isLegacyMode() {
1002            if (myLegacyMode == null) {
1003                if (Boolean.parseBoolean(System.getProperty(DEFAULT_LEGACY_MODE_PROPERTY))) {
1004                    return true;
1005                } else {
1006                    return false;
1007                }
1008            }
1009            return this.myLegacyMode;
1010        }
1011    
1012    
1013        /**
1014         * Returns the version ID (MSH-12) from the given message, without fully
1015         * parsing the message. The version is needed prior to parsing in order to
1016         * determine the message class into which the text of the message should be
1017         * parsed.
1018         * 
1019         * @throws HL7Exception
1020         *             if the version field can not be found.
1021         */
1022        public String getVersion(String message) throws HL7Exception {
1023            int startMSH = message.indexOf("MSH");
1024            int endMSH = message.indexOf(PipeParser.SEGMENT_DELIMITER, startMSH);
1025            if (endMSH < 0)
1026                endMSH = message.length();
1027            String msh = message.substring(startMSH, endMSH);
1028            String fieldSep = null;
1029            if (msh.length() > 3) {
1030                fieldSep = String.valueOf(msh.charAt(3));
1031            } else {
1032                throw new HL7Exception("Can't find field separator in MSH: " + msh, HL7Exception.UNSUPPORTED_VERSION_ID);
1033            }
1034    
1035            String[] fields = split(msh, fieldSep);
1036    
1037            String compSep = null;
1038            if (fields.length >= 2 && fields[1] != null && fields[1].length() == 4) {
1039                compSep = String.valueOf(fields[1].charAt(0)); // get component
1040                                                               // separator as 1st
1041                                                               // encoding char
1042            } else {
1043                throw new HL7Exception("Invalid or incomplete encoding characters - MSH-2 is " + fields[1], HL7Exception.REQUIRED_FIELD_MISSING);
1044            }
1045    
1046            String version = null;
1047            if (fields.length >= 12) {
1048                String[] comp = split(fields[11], compSep);
1049                if (comp.length >= 1) {
1050                    version = comp[0];
1051                } else {
1052                    throw new HL7Exception("Can't find version ID - MSH.12 is " + fields[11], HL7Exception.REQUIRED_FIELD_MISSING);
1053                }
1054            } else {
1055                throw new HL7Exception("Can't find version ID - MSH has only " + fields.length + " fields.", HL7Exception.REQUIRED_FIELD_MISSING);
1056            }
1057            return version;
1058        }
1059    
1060    
1061        @Override
1062        public void parse(Message message, String string) throws HL7Exception {
1063            IStructureDefinition structureDef = getStructureDefinition(message);
1064    
1065            // MessagePointer ptr = new MessagePointer(this, m,
1066            // getEncodingChars(message));
1067            MessageIterator messageIter = new MessageIterator(message, structureDef, "MSH", true);
1068    
1069            String[] segments = split(string, SEGMENT_DELIMITER);
1070    
1071            if (segments.length == 0) {
1072                    throw new HL7Exception("Invalid message content: \"" + string + "\"");
1073            }
1074            
1075            if (segments[0] == null || segments[0].length() < 4) {
1076                    throw new HL7Exception("Invalid message content: \"" + string + "\"");
1077            }
1078    
1079            char delim = '|';
1080            String prevName = null;
1081            int repNum = 1;
1082            for (int i = 0; i < segments.length; i++) {
1083    
1084                // get rid of any leading whitespace characters ...
1085                if (segments[i] != null && segments[i].length() > 0 && Character.isWhitespace(segments[i].charAt(0)))
1086                    segments[i] = stripLeadingWhitespace(segments[i]);
1087    
1088                // sometimes people put extra segment delimiters at end of msg ...
1089                if (segments[i] != null && segments[i].length() >= 3) {
1090                    
1091                    final String name;
1092                    if (i == 0) {
1093                        if (segments[i].length() < 4) {
1094                            throw new HL7Exception("Invalid message content: \"" + string + "\"");
1095                        }
1096                        name = segments[i].substring(0, 3);
1097                        delim = segments[i].charAt(3);
1098                    } else {
1099                        if (segments[i].indexOf(delim) >= 0) {
1100                            name = segments[i].substring(0, segments[i].indexOf(delim));
1101                        } else {
1102                            name = segments[i];
1103                        }
1104                    }
1105    
1106                    log.debug("Parsing segment {}", name);
1107    
1108                    if (name.equals(prevName)) {
1109                            repNum++;
1110                    } else {
1111                            repNum = 1;
1112                            prevName = name;
1113                    }
1114                    
1115                    messageIter.setDirection(name);
1116    
1117                    if (messageIter.hasNext()) {
1118                        Segment next = (Segment) messageIter.next();
1119                        parse(next, segments[i], getEncodingChars(string), repNum);
1120                    }
1121                }
1122            }
1123        }
1124        
1125        
1126        /**
1127         * A struct for holding a message class string and a boolean indicating
1128         * whether it was defined explicitly.
1129         */
1130        private static class MessageStructure {
1131            public String messageStructure;
1132            public boolean explicitlyDefined;
1133    
1134    
1135            public MessageStructure(String theMessageStructure, boolean isExplicitlyDefined) {
1136                messageStructure = theMessageStructure;
1137                explicitlyDefined = isExplicitlyDefined;
1138            }
1139        }
1140    
1141        private static class Holder<T> {
1142            private T myObject;
1143    
1144    
1145            public T getObject() {
1146                return myObject;
1147            }
1148    
1149    
1150            public void setObject(T theObject) {
1151                myObject = theObject;
1152            }
1153        }
1154    
1155    }