001/** 002The contents of this file are subject to the Mozilla Public License Version 1.1 003(the "License"); you may not use this file except in compliance with the License. 004You may obtain a copy of the License at http://www.mozilla.org/MPL/ 005Software distributed under the License is distributed on an "AS IS" basis, 006WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 007specific language governing rights and limitations under the License. 008 009The Original Code is "AbstractMessage.java". Description: 010"A default implementation of Message" 011 012The Initial Developer of the Original Code is University Health Network. Copyright (C) 0132001. All Rights Reserved. 014 015Contributor(s): ______________________________________. 016 017Alternatively, the contents of this file may be used under the terms of the 018GNU General Public License (the �GPL�), in which case the provisions of the GPL are 019applicable instead of those above. If you wish to allow use of your version of this 020file only under the terms of the GPL and not to allow others to use your version 021of this file under the MPL, indicate your decision by deleting the provisions above 022and replace them with the notice and other provisions required by the GPL License. 023If you do not delete the provisions above, a recipient may use your version of 024this file under either the MPL or the GPL. 025 026*/ 027 028package ca.uhn.hl7v2.model; 029 030import java.io.IOException; 031import java.util.Arrays; 032import java.util.Date; 033import java.util.GregorianCalendar; 034import java.util.Map; 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037 038import ca.uhn.hl7v2.AcknowledgmentCode; 039import ca.uhn.hl7v2.HL7Exception; 040import ca.uhn.hl7v2.Location; 041import ca.uhn.hl7v2.Version; 042import ca.uhn.hl7v2.model.primitive.CommonTS; 043import ca.uhn.hl7v2.parser.DefaultModelClassFactory; 044import ca.uhn.hl7v2.parser.ModelClassFactory; 045import ca.uhn.hl7v2.parser.Parser; 046import ca.uhn.hl7v2.parser.PipeParser; 047import ca.uhn.hl7v2.util.ArrayUtil; 048import ca.uhn.hl7v2.util.ReflectionUtil; 049import ca.uhn.hl7v2.util.StringUtil; 050import ca.uhn.hl7v2.util.Terser; 051import ca.uhn.hl7v2.validation.ValidationContext; 052 053/** 054 * A default implementation of Message. 055 * @author Bryan Tripp (bryan_tripp@sourceforge.net) 056 */ 057@SuppressWarnings("serial") 058public abstract class AbstractMessage extends AbstractGroup implements Message { 059 060 private static final Pattern ourVersionPattern = Pattern.compile("\\.(v2[0-9][0-9]?)\\."); 061 private String myVersion; 062 private transient Parser myParser; 063 064 /** 065 * @param theFactory factory for model classes (e.g. group, segment) for this message 066 */ 067 public AbstractMessage(ModelClassFactory theFactory) { 068 super(null, theFactory); 069 } 070 071 /** 072 * Returns this Message object. 073 */ 074 public Message getMessage() { 075 return this; 076 } 077 078 public Group getParent() { 079 return this; 080 } 081 082 /** 083 * Returns the version number. This default implementation inspects 084 * this.getClass().getName(). This should be overridden if you are putting 085 * a custom message definition in your own package, or it will default. 086 * @see Message#getVersion() 087 * 088 * @return lowest available version if not obvious from package name 089 */ 090 public String getVersion() { 091 if (myVersion != null) { 092 return myVersion; 093 } 094 095 String version = null; 096 Matcher m = ourVersionPattern.matcher(this.getClass().getName()); 097 if (m.find()) { 098 String verFolder = m.group(1); 099 if (verFolder.length() > 0) { 100 char[] chars = verFolder.toCharArray(); 101 StringBuilder buf = new StringBuilder(); 102 for (int i = 1; i < chars.length; i++) { //start at 1 to avoid the 'v' 103 buf.append(chars[i]); 104 if (i < chars.length - 1) buf.append('.'); 105 } 106 version = buf.toString(); 107 } 108 } 109 110 if (version == null) 111 version = Version.lowestAvailableVersion().getVersion(); 112 113 myVersion = version; 114 return version; 115 } 116 117 /** 118 * Returns the set of validation rules that applied to this message. If the parser 119 * was set to "not-validating", this method returns null 120 * 121 * @return the set of validation rules that applied to this message 122 */ 123 public ValidationContext getValidationContext() { 124 if (getParser() == null || !getParser().getParserConfiguration().isValidating()) return null; 125 return getParser().getHapiContext().getValidationContext(); 126 } 127 128 129 /** 130 * {@inheritDoc } 131 */ 132 public Character getFieldSeparatorValue() throws HL7Exception { 133 Segment firstSegment = (Segment) get(getNames()[0]); 134 Primitive value = (Primitive) firstSegment.getField(1, 0); 135 String valueString = value.getValue(); 136 if (valueString == null || valueString.length() == 0) { 137 return null; 138 } 139 return valueString.charAt(0); 140 } 141 142 143 /** 144 * {@inheritDoc } 145 */ 146 public String getEncodingCharactersValue() throws HL7Exception { 147 Segment firstSegment = (Segment) get(getNames()[0]); 148 Primitive value = (Primitive) firstSegment.getField(2, 0); 149 return value.getValue(); 150 } 151 152 153 /** 154 * <p>Sets the parser to be used when parse/encode methods are called on this 155 * Message, as well as its children. It is recommended that if these methods 156 * are going to be called, a parser be supplied with the validation context 157 * wanted. Where possible, the parser should be reused for best performance, 158 * unless thread safety is an issue.</p> 159 * 160 * <p>Note that not all parsers can be used. As of version 1.0, only {@link PipeParser} 161 * supports this functionality</p> 162 * 163 * <p>Serialization note: The message parser is marked as transient, so it will not 164 * survive serialization.</p> 165 */ 166 public void setParser(Parser parser) { 167 if (parser == null) { 168 throw new NullPointerException("Value may not be null"); 169 } 170 171 myParser = parser; 172 } 173 174 175 /** 176 * <p>Returns the parser to be used when parse/encode methods are called on this 177 * Message, as well as its children. The default value is a new {@link PipeParser}.</p> 178 * 179 * <p>Serialization note: The message parser is marked as transient, so it will not 180 * survive serialization.</p> 181 */ 182 public Parser getParser() { 183 if (myParser == null) { 184 myParser = new PipeParser(); 185 } 186 187 return myParser; 188 } 189 190 191 /** 192 * {@inheritDoc } 193 */ 194 public void parse(String string) throws HL7Exception { 195 clear(); 196 getParser().parse(this, string); 197 } 198 199 200 /** 201 * {@inheritDoc } 202 */ 203 public String encode() throws HL7Exception { 204 return getParser().encode(this); 205 } 206 207 /** 208 * {@inheritDoc } 209 */ 210 public Message generateACK() throws HL7Exception, IOException { 211 return generateACK(AcknowledgmentCode.AA, null); 212 } 213 214 /** 215 * {@inheritDoc } 216 * @deprecated 217 */ 218 public Message generateACK(String theAcknowledgementCode, HL7Exception theException) throws HL7Exception, IOException { 219 AcknowledgmentCode theCode = theAcknowledgementCode == null ? 220 AcknowledgmentCode.AA : 221 AcknowledgmentCode.valueOf(theAcknowledgementCode); 222 return generateACK(theCode, theException); 223 } 224 225 /** 226 * {@inheritDoc } 227 */ 228 public Message generateACK(AcknowledgmentCode theAcknowledgementCode, HL7Exception theException) throws HL7Exception, IOException { 229 if (theException != null && theException.getResponseMessage() != null) { 230 return theException.getResponseMessage(); 231 } 232 Message out = instantiateACK(); 233 out.setParser(getParser()); 234 fillResponseHeader(out, theAcknowledgementCode); 235 if (theException != null) { 236 theException.populateResponse(out, theAcknowledgementCode, 0); 237 } 238 return out; 239 } 240 241 private Message instantiateACK() throws HL7Exception { 242 ModelClassFactory mcf = getParser() != null ? 243 getParser().getFactory() : 244 new DefaultModelClassFactory(); 245 Version version = Version.versionOf(getVersion()); 246 Message out = null; 247 if (version != null && version.available()) { 248 Class<? extends Message> clazz = mcf.getMessageClass("ACK", version.getVersion(), false); 249 if (clazz != null) { 250 out = ReflectionUtil.instantiateMessage(clazz, mcf); 251 } 252 } 253 if (out == null) { 254 out = new GenericMessage.UnknownVersion(mcf); 255 } 256 257 if (out instanceof GenericMessage) { 258 if (!ArrayUtil.contains(out.getNames(), "MSA")) { 259 out.addNonstandardSegment("MSA"); 260 } 261 if (!ArrayUtil.contains(out.getNames(), "ERR")) { 262 out.addNonstandardSegment("ERR"); 263 } 264 } 265 266 return out; 267 } 268 269 /** 270 * Populates certain required fields in a response message header, using 271 * information from the corresponding inbound message. The current time is 272 * used for the message time field, and <code>MessageIDGenerator</code> is 273 * used to create a unique message ID. Version and message type fields are 274 * not populated. 275 * 276 * @param out outgoing message to be populated 277 * @param code acknowledgment code 278 * @return outgoing message 279 * @throws HL7Exception if header cannot be filled 280 * @throws IOException if message ID could not be generated 281 */ 282 public Message fillResponseHeader(Message out, AcknowledgmentCode code) 283 throws HL7Exception, IOException { 284 Segment mshIn = (Segment) get("MSH"); 285 Segment mshOut = (Segment) out.get("MSH"); 286 287 // get MSH data from incoming message ... 288 String fieldSep = Terser.get(mshIn, 1, 0, 1, 1); 289 String encChars = Terser.get(mshIn, 2, 0, 1, 1); 290 String procID = Terser.get(mshIn, 11, 0, 1, 1); 291 292 // populate outbound MSH using data from inbound message ... 293 Terser.set(mshOut, 1, 0, 1, 1, fieldSep); 294 Terser.set(mshOut, 2, 0, 1, 1, encChars); 295 GregorianCalendar now = new GregorianCalendar(); 296 now.setTime(new Date()); 297 Terser.set(mshOut, 7, 0, 1, 1, CommonTS.toHl7TSFormat(now)); 298 Terser.set(mshOut, 9, 0, 1, 1, "ACK"); 299 Terser.set(mshOut, 9, 0, 2, 1, Terser.get(mshIn, 9, 0, 2, 1)); 300 String v = mshOut.getMessage().getVersion(); 301 if (v != null) { 302 Version version = Version.versionOf(v); 303 if (version != null && !Version.V25.isGreaterThan(version)) { 304 Terser.set(mshOut, 9, 0, 3, 1, "ACK"); 305 } 306 } 307 Terser.set(mshOut, 10, 0, 1, 1, mshIn.getMessage().getParser().getParserConfiguration().getIdGenerator().getID()); 308 Terser.set(mshOut, 11, 0, 1, 1, procID); 309 310 String versionId = Terser.get(mshIn, 12, 0, 1, 1); 311 if (StringUtil.isBlank(versionId)) { 312 versionId = Version.highestAvailableVersionOrDefault().getVersion(); 313 } 314 Terser.set(mshOut, 12, 0, 1, 1, versionId); 315 316 // revert sender and receiver 317 Terser.set(mshOut, 3, 0, 1, 1, Terser.get(mshIn, 5, 0, 1, 1)); 318 Terser.set(mshOut, 4, 0, 1, 1, Terser.get(mshIn, 6, 0, 1, 1)); 319 Terser.set(mshOut, 5, 0, 1, 1, Terser.get(mshIn, 3, 0, 1, 1)); 320 Terser.set(mshOut, 6, 0, 1, 1, Terser.get(mshIn, 4, 0, 1, 1)); 321 322 // fill MSA for the happy case 323 Segment msaOut = (Segment) out.get("MSA"); 324 Terser.set(msaOut, 1, 0, 1, 1, code.name()); 325 Terser.set(msaOut, 2, 0, 1, 1, Terser.get(mshIn, 10, 0, 1, 1)); 326 return out; 327 } 328 329 330 /** 331 * Provides an overview of the type and structure of this message 332 */ 333 @Override 334 public String toString() { 335 try { 336 return encode(); 337 } catch (HL7Exception e) { 338 return (getClass().getName() + " - Failed to create toString(): " + e.getMessage()); 339 } 340 } 341 342 /** 343 * {@inheritDoc} 344 */ 345 public String printStructure() throws HL7Exception { 346 StringBuilder builder = new StringBuilder(); 347 appendStructureDescription(builder, 0, false, false, true, true, true); 348 return builder.toString(); 349 } 350 351 /** 352 * Prints the message structure in a similar way to {@link #printStructure()} but 353 * optionally excludes elements with no contents. 354 */ 355 public String printStructure(boolean includeEmptyElements) throws HL7Exception { 356 StringBuilder builder = new StringBuilder(); 357 appendStructureDescription(builder, 0, false, false, true, true, includeEmptyElements); 358 return builder.toString(); 359 } 360 361 /** 362 * Quickly initializes this message with common values in the first (MSH) segment. 363 * 364 * <p> 365 * Settings include: 366 * <ul> 367 * <li>MSH-1 (Field Separator) is set to "|"</li> 368 * <li>MSH-2 (Encoding Characters) is set to "^~\&"</li> 369 * <li>MSH-7 (Date/Time of Message) is set to current time</li> 370 * <li>MSH-10 (Control ID) is populated using next value generated by a 371 * {@link ca.uhn.hl7v2.util.idgenerator.IDGenerator IDGenerator}</li> 372 * </ul> 373 * </p> 374 * 375 * @param messageCode The message code (aka message type) to insert into MSH-9-1. Example: "ADT" 376 * @param messageTriggerEvent The message trigger event to insert into MSG-9-2. Example: "A01" 377 * @param processingId The message processing ID to insert into MSH-11. Examples: "T" (for TEST) or "P" for (PRODUCTION) 378 * 379 * @throws IOException If the message ID generation fails for some reason 380 * @throws HL7Exception If the message rejects any of the values which are generated to setting 381 */ 382 public void initQuickstart(String messageCode, String messageTriggerEvent, String processingId) throws HL7Exception, IOException { 383 Segment msh = (Segment) get("MSH"); 384 Version version = Version.versionOf(getVersion()); 385 Terser.set(msh, 1, 0, 1, 1, "|"); 386 Terser.set(msh, 2, 0, 1, 1, Version.V27.isGreaterThan(version) ? 387 "^~\\&" : "^~\\&#"); 388 GregorianCalendar now = new GregorianCalendar(); 389 Terser.set(msh, 7, 0, 1, 1, CommonTS.toHl7TSFormat(now)); 390 Terser.set(msh, 9, 0, 1, 1, messageCode); 391 Terser.set(msh, 9, 0, 2, 1, messageTriggerEvent); 392 Terser.set(msh, 10, 0, 1, 1, getParser().getParserConfiguration().getIdGenerator().getID()); 393 Terser.set(msh, 11, 0, 1, 1, processingId); 394 Terser.set(msh, 12, 0, 1, 1, getVersion()); 395 396 // Add structure information if version is 2.4 or better 397 if (!Version.V24.isGreaterThan(version)) { 398 if (this instanceof SuperStructure) { 399 Map<String, String> eventMap = new DefaultModelClassFactory().getEventMapForVersion(version); 400 if (StringUtil.isNotBlank(messageCode) && StringUtil.isNotBlank(messageTriggerEvent)) { 401 String structure = eventMap.get(messageCode + "_" + messageTriggerEvent); 402 Terser.set(msh, 9, 0, 3, 1, structure); 403 } 404 } else { 405 String className = getClass().getName(); 406 int lastIndexOf = className.lastIndexOf('.'); 407 className = className.substring(lastIndexOf + 1); 408 if (className.matches("[A-Z]{3}_[A-Z0-9]{3}")) { 409 Terser.set(msh, 9, 0, 3, 1, className); 410 } 411 } 412 } 413 414 } 415 416 @Override 417 public boolean accept(MessageVisitor visitor, Location location) throws HL7Exception { 418 if (visitor.start(this)) { 419 visitNestedStructures(visitor, location); 420 } 421 return visitor.end(this); 422 } 423 424 /** 425 * Creates a copy of <code>this</code> {@link AbstractMessage} by recursively looping over each 426 * {@link Structure} (i.e. {@link AbstractGroup} or {@link AbstractSegment}). When an 427 * {@link AbstractSegment} is found, its contents are encoded and parsed into the copied 428 * {@link AbstractMessage} 429 * 430 * @return A copy of <code>this</code> {@link AbstractMessage} 431 * @throws HL7Exception If an error occurs while the message is being copied 432 */ 433 public AbstractMessage copy() throws HL7Exception{ 434 AbstractMessage clonedMessage = ReflectionUtil.instantiateMessage(this.getClass(), this.getModelClassFactory()); 435 clonedMessage.setParser(this.getParser()); 436 437 copyParserRequiredMshFields(this, clonedMessage); 438 copyGroup(this, clonedMessage); 439 return clonedMessage; 440 } 441 442 private void copyParserRequiredMshFields(AbstractMessage theSource, AbstractMessage theTarget) throws HL7Exception { 443 String mshField = "MSH"; 444 AbstractSegment sourceMsh = (AbstractSegment) theSource.get(mshField); 445 AbstractSegment targetMsh = (AbstractSegment) theTarget.get(mshField); 446 447 String msh1 = Terser.get(sourceMsh, 1, 0, 1, 1); 448 Terser.set(targetMsh, 1, 0, 1, 1, msh1); 449 450 String msh2 = Terser.get(sourceMsh, 2, 0, 1, 1); 451 Terser.set(targetMsh, 2, 0, 1, 1, msh2); 452 } 453 454 private void copyGroup(AbstractGroup theSource, AbstractGroup theTarget) throws HL7Exception { 455 String[] sourceNames = theSource.getNames(); 456 457 for (String sourceName : sourceNames) { 458 459 Structure[] sourceStructures = theSource.getAll(sourceName); 460 for (int i = 0; i < sourceStructures.length; i++) { 461 462 Structure sourceStructure = sourceStructures[i]; 463 464 if (sourceStructure instanceof AbstractGroup) { 465 Structure targetGroup = theTarget.get(sourceName, i); 466 copyGroup((AbstractGroup)sourceStructure, (AbstractGroup)targetGroup); 467 468 } else if (sourceStructure instanceof AbstractSegment) { 469 470 boolean isNonStandardSegment = theSource.getNonStandardNames().contains(sourceName); 471 if (isNonStandardSegment) { 472 insertNonStandardSegment(theSource, theTarget, sourceName); 473 } 474 475 AbstractSegment sourceSegment = (AbstractSegment) sourceStructure; 476 AbstractSegment targetSegment = (AbstractSegment) theTarget.get(sourceName, i); 477 String segmentContents = sourceSegment.encode(); 478 targetSegment.parse(segmentContents); 479 } 480 } 481 } 482 } 483 484 private void insertNonStandardSegment(AbstractGroup theSource, AbstractGroup theTarget, String theNonStandardSegmentName) throws HL7Exception { 485 String[] sourceNames = theSource.getNames(); 486 int sourceIndex = Arrays.asList(sourceNames).indexOf(theNonStandardSegmentName); 487 488 String[] targetNames = theTarget.getNames(); 489 boolean shouldInsertAtEnd = sourceIndex >= targetNames.length; 490 491 if (shouldInsertAtEnd) { 492 theTarget.addNonstandardSegment(theNonStandardSegmentName); 493 } else { 494 theTarget.addNonstandardSegment(theNonStandardSegmentName, sourceIndex); 495 } 496 } 497}