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
028package ca.uhn.hl7v2.parser;
029
030import java.util.ArrayList;
031import java.util.HashMap;
032import java.util.StringTokenizer;
033
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import ca.uhn.hl7v2.HL7Exception;
038import ca.uhn.hl7v2.model.DoNotCacheStructure;
039import ca.uhn.hl7v2.model.Group;
040import ca.uhn.hl7v2.model.Message;
041import ca.uhn.hl7v2.model.Primitive;
042import ca.uhn.hl7v2.model.Segment;
043import ca.uhn.hl7v2.model.Structure;
044import ca.uhn.hl7v2.model.Type;
045import ca.uhn.hl7v2.model.Varies;
046import ca.uhn.hl7v2.util.ReflectionUtil;
047import ca.uhn.hl7v2.util.Terser;
048import ca.uhn.hl7v2.validation.impl.NoValidation;
049import 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 */
058public 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}