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 org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  import ca.uhn.hl7v2.DefaultHapiContext;
42  import ca.uhn.hl7v2.ErrorCode;
43  import ca.uhn.hl7v2.HL7Exception;
44  import ca.uhn.hl7v2.HapiContext;
45  import ca.uhn.hl7v2.Version;
46  import ca.uhn.hl7v2.model.AbstractSuperMessage;
47  import ca.uhn.hl7v2.model.DoNotCacheStructure;
48  import ca.uhn.hl7v2.model.Group;
49  import ca.uhn.hl7v2.model.Message;
50  import ca.uhn.hl7v2.model.Primitive;
51  import ca.uhn.hl7v2.model.Segment;
52  import ca.uhn.hl7v2.model.Structure;
53  import ca.uhn.hl7v2.model.SuperStructure;
54  import ca.uhn.hl7v2.model.Type;
55  import ca.uhn.hl7v2.model.Varies;
56  import ca.uhn.hl7v2.util.ReflectionUtil;
57  import ca.uhn.hl7v2.util.Terser;
58  import ca.uhn.hl7v2.validation.impl.NoValidation;
59  import ca.uhn.hl7v2.validation.impl.ValidationContextFactory;
60  
61  /**
62   * An implementation of Parser that supports traditionally encoded (ie delimited
63   * with characters like |, ^, and ~) HL7 messages. Unexpected segments and
64   * fields are parsed into generic elements that are added to the message.
65   * 
66   * @see ParserConfiguration for configuration options which may affect parser encoding and decoding behaviour
67   * @author Bryan Tripp (bryan_tripp@sourceforge.net)
68   */
69  public class PipeParser extends Parser {
70  
71  	private static final Logger log = LoggerFactory.getLogger(PipeParser.class);
72  
73  	/**
74  	 * The HL7 ER7 segment delimiter (see section 2.8 of spec)
75  	 */
76  	final static String SEGMENT_DELIMITER = "\r";
77  
78  	private final HashMap<Class<? extends Message>, HashMap<String, StructureDefinition>> myStructureDefinitions = new HashMap<Class<? extends Message>, HashMap<String, StructureDefinition>>();
79  
80  	/**
81  	 * System property key. If value is "true", legacy mode will default to true
82  	 * 
83  	 * @see #isLegacyMode()
84  	 * @deprecated This will be removed in HAPI 3.0
85  	 */
86  	public static final String DEFAULT_LEGACY_MODE_PROPERTY = "ca.uhn.hl7v2.parser.PipeParser.default_legacy_mode";
87  
88  	private Boolean myLegacyMode = null;
89  
90  	public PipeParser() {
91  		super();
92  	}
93  
94  	/**
95  	 * @param context
96  	 *            the context containing all configuration items to be used
97  	 */
98  	public PipeParser(HapiContext context) {
99  		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 }