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