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