View Javadoc

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