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