001 /** 002 The contents of this file are subject to the Mozilla Public License Version 1.1 003 (the "License"); you may not use this file except in compliance with the License. 004 You may obtain a copy of the License at http://www.mozilla.org/MPL/ 005 Software distributed under the License is distributed on an "AS IS" basis, 006 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 007 specific language governing rights and limitations under the License. 008 009 The Original Code is "DefaultValidator.java". Description: 010 "A default conformance validator." 011 012 The Initial Developer of the Original Code is University Health Network. Copyright (C) 013 2003. All Rights Reserved. 014 015 Contributor(s): ______________________________________. 016 017 Alternatively, the contents of this file may be used under the terms of the 018 GNU General Public License (the ???GPL???), in which case the provisions of the GPL are 019 applicable instead of those above. If you wish to allow use of your version of this 020 file only under the terms of the GPL and not to allow others to use your version 021 of this file under the MPL, indicate your decision by deleting the provisions above 022 and replace them with the notice and other provisions required by the GPL License. 023 If you do not delete the provisions above, a recipient may use your version of 024 this file under either the MPL or the GPL. 025 026 */ 027 028 package ca.uhn.hl7v2.conf.check; 029 030 import ca.uhn.hl7v2.model.*; 031 import ca.uhn.hl7v2.conf.spec.message.*; 032 import ca.uhn.hl7v2.conf.spec.*; 033 import ca.uhn.hl7v2.util.Terser; 034 import ca.uhn.hl7v2.HL7Exception; 035 import ca.uhn.hl7v2.conf.ProfileException; 036 import ca.uhn.hl7v2.conf.parser.ProfileParser; 037 import ca.uhn.hl7v2.parser.*; 038 import java.util.ArrayList; 039 import java.io.*; 040 import ca.uhn.log.*; 041 import ca.uhn.hl7v2.conf.store.*; 042 043 /** 044 * A default conformance validator. 045 * @author Bryan Tripp 046 */ 047 public class DefaultValidator implements Validator { 048 049 private EncodingCharacters enc; //used to check for content in parts of a message 050 private static final HapiLog log = HapiLogFactory.getHapiLog( DefaultValidator.class ); 051 052 /** Creates a new instance of DefaultValidator */ 053 public DefaultValidator() { 054 enc = new EncodingCharacters('|', null); //the | is assumed later -- don't change 055 } 056 057 /** 058 * @see Validator#validate 059 */ 060 public HL7Exception[] validate(Message message, StaticDef profile) throws ProfileException, HL7Exception { 061 ArrayList exList = new ArrayList(20); 062 Terser t = new Terser(message); 063 064 //check msg type, event type, msg struct ID 065 String msgType = t.get("/MSH-9-1"); 066 if (!msgType.equals(profile.getMsgType())) { 067 HL7Exception e = 068 new ProfileNotFollowedException("Message type " + msgType + " doesn't match profile type of " + profile.getMsgType()); 069 exList.add(e); 070 } 071 072 String evType = t.get("/MSH-9-2"); 073 if (!evType.equals(profile.getEventType()) && !profile.getEventType().equalsIgnoreCase("ALL")) { 074 HL7Exception e = 075 new ProfileNotFollowedException("Event type " + evType + " doesn't match profile type of " + profile.getEventType()); 076 exList.add(e); 077 } 078 079 String msgStruct = t.get("/MSH-9-3"); 080 if (msgStruct == null || !msgStruct.equals(profile.getMsgStructID())) { 081 HL7Exception e = 082 new ProfileNotFollowedException("Message structure " + msgStruct + " doesn't match profile type of " + profile.getMsgStructID()); 083 exList.add(e); 084 } 085 086 Exception[] childExceptions; 087 childExceptions = testGroup(message, profile, profile.getIdentifier()); 088 for (int i = 0; i < childExceptions.length; i++) { 089 exList.add(childExceptions[i]); 090 } 091 092 return toArray(exList); 093 } 094 095 /** 096 * Tests a group against a group section of a profile. 097 */ 098 public HL7Exception[] testGroup(Group group, AbstractSegmentContainer profile, String profileID) throws ProfileException { 099 ArrayList exList = new ArrayList(20); 100 ArrayList allowedStructures = new ArrayList(20); 101 102 for (int i = 1; i <= profile.getChildren(); i++) { 103 ProfileStructure struct = profile.getChild(i); 104 105 //only test a structure in detail if it isn't X 106 if (!struct.getUsage().equalsIgnoreCase("X")) { 107 allowedStructures.add(struct.getName()); 108 109 //see which instances have content 110 try { 111 Structure[] instances = group.getAll(struct.getName()); 112 ArrayList instancesWithContent = new ArrayList(10); 113 for (int j = 0; j < instances.length; j++) { 114 if (hasContent(instances[j])) instancesWithContent.add(instances[j]); 115 } 116 117 HL7Exception ce = testCardinality(instancesWithContent.size(), 118 struct.getMin(), struct.getMax(), struct.getUsage(), struct.getName()); 119 if (ce != null) exList.add(ce); 120 121 //test children on instances with content 122 for (int j = 0; j < instancesWithContent.size(); j++) { 123 Structure s = (Structure) instancesWithContent.get(j); 124 HL7Exception[] childExceptions = testStructure(s, struct, profileID); 125 addToList(childExceptions, exList); 126 } 127 } catch (HL7Exception he) { 128 exList.add(new ProfileNotHL7CompliantException(struct.getName() + " not found in message")); 129 } 130 } 131 } 132 133 //complain about X structures that have content 134 addToList(checkForExtraStructures(group, allowedStructures), exList); 135 136 return toArray(exList); 137 } 138 139 /** 140 * Checks a group's children against a list of allowed structures for the group 141 * (ie those mentioned in the profile with usage other than X). Returns 142 * a list of exceptions representing structures that appear in the message 143 * but are not supposed to. 144 */ 145 private HL7Exception[] checkForExtraStructures(Group group, ArrayList allowedStructures) throws ProfileException { 146 ArrayList exList = new ArrayList(); 147 String[] childNames = group.getNames(); 148 for (int i = 0; i < childNames.length; i++) { 149 if (!allowedStructures.contains(childNames[i])) { 150 try { 151 Structure[] reps = group.getAll(childNames[i]); 152 for (int j = 0; j < reps.length; j++) { 153 if (hasContent(reps[j])) { 154 HL7Exception e = 155 new XElementPresentException("The structure " 156 + childNames[i] 157 + " appears in the message but not in the profile"); 158 exList.add(e); 159 } 160 } 161 } catch (HL7Exception he) { 162 throw new ProfileException("Problem checking profile", he); 163 } 164 } 165 } 166 return toArray(exList); 167 } 168 169 /** 170 * Checks cardinality and creates an appropriate exception if out 171 * of bounds. The usage code is needed because if min cardinality 172 * is > 0, the min # of reps is only required if the usage code 173 * is 'R' (see HL7 v2.5 section 2.12.6.4). 174 * @param reps the number of reps 175 * @param min the minimum number of reps 176 * @param max the maximum number of reps (-1 means *) 177 * @param usage the usage code 178 * @param name the name of the repeating structure (used in exception msg) 179 * @return null if cardinality OK, exception otherwise 180 */ 181 protected HL7Exception testCardinality(int reps, int min, int max, String usage, String name) { 182 HL7Exception e = null; 183 if (reps < min && usage.equalsIgnoreCase("R")) { 184 e = new ProfileNotFollowedException(name + " must have at least " + min + " repetitions (has " + reps + ")"); 185 } else if (max > 0 && reps > max) { 186 e = new ProfileNotFollowedException(name + " must have no more than " + max + " repetitions (has " + reps + ")"); 187 } 188 return e; 189 } 190 191 /** 192 * Tests a structure (segment or group) against the corresponding part of a profile. 193 */ 194 public HL7Exception[] testStructure(Structure s, ProfileStructure profile, String profileID) throws ProfileException { 195 ArrayList exList = new ArrayList(20); 196 if (profile instanceof Seg) { 197 if (Segment.class.isAssignableFrom(s.getClass())) { 198 addToList(testSegment((Segment) s, (Seg) profile, profileID), exList); 199 } else { 200 exList.add(new ProfileNotHL7CompliantException("Mismatch between a segment in the profile and the structure " 201 + s.getClass().getName() + " in the message")); 202 } 203 } else if (profile instanceof SegGroup) { 204 if (Group.class.isAssignableFrom(s.getClass())) { 205 addToList(testGroup((Group) s, (SegGroup) profile, profileID), exList); 206 } else { 207 exList.add(new ProfileNotHL7CompliantException("Mismatch between a group in the profile and the structure " 208 + s.getClass().getName() + " in the message")); 209 } 210 } 211 return toArray(exList); 212 } 213 214 /** 215 * Tests a segment against a segment section of a profile. 216 */ 217 public HL7Exception[] testSegment(ca.uhn.hl7v2.model.Segment segment, Seg profile, String profileID) throws ProfileException { 218 ArrayList exList = new ArrayList(20); 219 ArrayList allowedFields = new ArrayList(20); 220 221 for (int i = 1; i <= profile.getFields(); i++) { 222 Field field = profile.getField(i); 223 224 //only test a field in detail if it isn't X 225 if (!field.getUsage().equalsIgnoreCase("X")) { 226 allowedFields.add(new Integer(i)); 227 228 //see which instances have content 229 try { 230 Type[] instances = segment.getField(i); 231 ArrayList instancesWithContent = new ArrayList(10); 232 for (int j = 0; j < instances.length; j++) { 233 if (hasContent(instances[j])) instancesWithContent.add(instances[j]); 234 } 235 236 HL7Exception ce = testCardinality(instancesWithContent.size(), 237 field.getMin(), field.getMax(), field.getUsage(), field.getName()); 238 if (ce != null) { 239 ce.setFieldPosition(i); 240 exList.add(ce); 241 } 242 243 //test field instances with content 244 for (int j = 0; j < instancesWithContent.size(); j++) { 245 Type s = (Type) instancesWithContent.get(j); 246 247 boolean escape = true; //escape field value when checking length 248 if (profile.getName().equalsIgnoreCase("MSH") && i < 3) { 249 escape = false; 250 } 251 HL7Exception[] childExceptions = testField(s, field, escape, profileID); 252 for (int k = 0; k < childExceptions.length; k++) { 253 childExceptions[k].setFieldPosition(i); 254 } 255 addToList(childExceptions, exList); 256 } 257 } catch (HL7Exception he) { 258 exList.add(new ProfileNotHL7CompliantException("Field " + i + " not found in message")); 259 } 260 } 261 262 } 263 264 //complain about X fields with content 265 this.addToList(checkForExtraFields(segment, allowedFields), exList); 266 267 HL7Exception[] ret = toArray(exList); 268 for (int i = 0; i < ret.length; i++) { 269 ret[i].setSegmentName(profile.getName()); 270 } 271 return ret; 272 } 273 274 /** 275 * Checks a segment against a list of allowed fields 276 * (ie those mentioned in the profile with usage other than X). Returns 277 * a list of exceptions representing field that appear 278 * but are not supposed to. 279 * @param allowedFields an array of Integers containing field #s of allowed fields 280 */ 281 private HL7Exception[] checkForExtraFields(Segment segment, ArrayList allowedFields) throws ProfileException { 282 ArrayList exList = new ArrayList(); 283 for (int i = 1; i <= segment.numFields(); i++) { 284 if (!allowedFields.contains(new Integer(i))) { 285 try { 286 Type[] reps = segment.getField(i); 287 for (int j = 0; j < reps.length; j++) { 288 if (hasContent(reps[j])) { 289 HL7Exception e = new XElementPresentException( 290 "Field " + i + " in " + segment.getName() + " appears in the message but not in the profile"); 291 exList.add(e); 292 } 293 } 294 } catch (HL7Exception he) { 295 throw new ProfileException("Problem testing against profile", he); 296 } 297 } 298 } 299 return this.toArray(exList); 300 } 301 302 /** 303 * Tests a Type against the corresponding section of a profile. 304 * @param encoded optional encoded form of type (if you want to specify this -- if null, 305 * default pipe-encoded form is used to check length and constant val) 306 */ 307 public HL7Exception[] testType(Type type, AbstractComponent profile, String encoded, String profileID) { 308 ArrayList exList = new ArrayList(); 309 if (encoded == null) encoded = PipeParser.encode(type, this.enc); 310 311 HL7Exception ue = testUsage(encoded, profile.getUsage(), profile.getName()); 312 if (ue != null) exList.add(ue); 313 314 if ( !profile.getUsage().equals("X") ) { 315 //check datatype 316 String typeClass = type.getClass().getName(); 317 if (typeClass.indexOf("." + profile.getDatatype()) < 0) { 318 typeClass = typeClass.substring(typeClass.lastIndexOf('.') + 1); 319 exList.add(new ProfileNotHL7CompliantException("HL7 datatype " + typeClass + " doesn't match profile datatype " + profile.getDatatype())); 320 } 321 322 //check length 323 if (encoded.length() > profile.getLength()) 324 exList.add(new ProfileNotFollowedException("The type " + profile.getName() + " has length " + encoded.length() + " which exceeds max of " + profile.getLength())); 325 326 //check constant value 327 if (profile.getConstantValue() != null && profile.getConstantValue().length() > 0) { 328 if (!encoded.equals(profile.getConstantValue())) 329 exList.add(new ProfileNotFollowedException("'" + encoded + "' doesn't equal constant value of '" + profile.getConstantValue() + "'")); 330 } 331 332 HL7Exception[] te = testTypeAgainstTable(type, profile, profileID); 333 for (int i = 0; i < te.length; i++) { 334 exList.add(te[i]); 335 } 336 } 337 338 return this.toArray(exList); 339 } 340 341 /** 342 * Tests whether the given type falls within a maximum length. 343 * @return null of OK, an HL7Exception otherwise 344 */ 345 public HL7Exception testLength(Type type, int maxLength) { 346 HL7Exception e = null; 347 String encoded = PipeParser.encode(type, this.enc); 348 if (encoded.length() > maxLength) { 349 e = new ProfileNotFollowedException("Length of " + encoded.length() + " exceeds maximum of " + maxLength); 350 } 351 return e; 352 } 353 354 /** 355 * Tests an element against the corresponding usage code. The element 356 * is required in its encoded form. 357 * @param encoded the pipe-encoded message element 358 * @param usage the usage code (e.g. "CE") 359 * @param name the name of the element (for use in exception messages) 360 * @returns null if there is no problem, an HL7Exception otherwise 361 */ 362 private HL7Exception testUsage(String encoded, String usage, String name) { 363 HL7Exception e = null; 364 if (usage.equalsIgnoreCase("R")) { 365 if (encoded.length() == 0) 366 e = new ProfileNotFollowedException("Required element " + name + " is missing"); 367 } else if (usage.equalsIgnoreCase("RE")) { 368 //can't test anything 369 } else if (usage.equalsIgnoreCase("O")) { 370 //can't test anything 371 } else if (usage.equalsIgnoreCase("C")) { 372 //can't test anything yet -- wait for condition syntax in v2.6 373 } else if (usage.equalsIgnoreCase("CE")) { 374 //can't test anything 375 } else if (usage.equalsIgnoreCase("X")) { 376 if (encoded.length() > 0) 377 e = new XElementPresentException("Element " + name + " is present but specified as not used (X)"); 378 } else if (usage.equalsIgnoreCase("B")) { 379 //can't test anything 380 } 381 return e; 382 } 383 384 /** 385 * Tests table values for ID, IS, and CE types. An empty list is returned for 386 * all other types or if the table name or number is missing. 387 */ 388 private HL7Exception[] testTypeAgainstTable(Type type, AbstractComponent profile, String profileID) { 389 ArrayList exList = new ArrayList(); 390 if (profile.getTable() != null && (type.getName().equals("IS") || type.getName().equals("ID"))) { 391 String codeSystem = makeTableName(profile.getTable()); 392 String value = ((Primitive) type).getValue(); 393 addTableTestResult(exList, profileID, codeSystem, value); 394 } else if (type.getName().equals("CE")) { 395 String value = Terser.getPrimitive(type, 1, 1).getValue(); 396 String codeSystem = Terser.getPrimitive(type, 3, 1).getValue(); 397 addTableTestResult(exList, profileID, codeSystem, value); 398 399 value = Terser.getPrimitive(type, 4, 1).getValue(); 400 codeSystem = Terser.getPrimitive(type, 6, 1).getValue(); 401 addTableTestResult(exList, profileID, codeSystem, value); 402 } 403 return this.toArray(exList); 404 } 405 406 private void addTableTestResult(ArrayList exList, String profileID, String codeSystem, String value) { 407 if (codeSystem != null && value != null) { 408 HL7Exception e = testValueAgainstTable(profileID, codeSystem, value); 409 if (e != null) exList.add(e); 410 } 411 } 412 413 private HL7Exception testValueAgainstTable(String profileID, String codeSystem, String value) { 414 HL7Exception ret = null; 415 CodeStore store = ProfileStoreFactory.getCodeStore(profileID, codeSystem); 416 if (store == null) { 417 log.warn("Not checking value " + value + ": no code store was found for profile " + profileID 418 + " code system " + codeSystem); 419 } else { 420 if (!store.isValidCode(codeSystem, value)) 421 ret = new ProfileNotFollowedException("Code " + value + " not found in table " + codeSystem + ", profile " + profileID); 422 } 423 return ret; 424 } 425 426 private String makeTableName(String hl7Table) { 427 StringBuffer buf = new StringBuffer("HL7"); 428 if (hl7Table.length() < 4) buf.append("0"); 429 if (hl7Table.length() < 3) buf.append("0"); 430 if (hl7Table.length() < 2) buf.append("0"); 431 buf.append(hl7Table); 432 return buf.toString(); 433 } 434 435 public HL7Exception[] testField(Type type, Field profile, boolean escape, String profileID) throws ProfileException { 436 ArrayList exList = new ArrayList(20); 437 438 //account for MSH 1 & 2 which aren't escaped 439 String encoded = null; 440 if (!escape && Primitive.class.isAssignableFrom(type.getClass())) encoded = ((Primitive) type).getValue(); 441 442 addToList(testType(type, profile, encoded, profileID), exList); 443 444 //test children 445 if (profile.getComponents() > 0 && !profile.getUsage().equals("X")) { 446 if (Composite.class.isAssignableFrom(type.getClass())) { 447 Composite comp = (Composite) type; 448 for (int i = 1; i <= profile.getComponents(); i++) { 449 Component childProfile = profile.getComponent(i); 450 try { 451 Type child = comp.getComponent(i-1); 452 addToList(testComponent(child, childProfile, profileID), exList); 453 } catch (DataTypeException de) { 454 exList.add(new ProfileNotHL7CompliantException("More components in profile than allowed in message: " + de.getMessage())); 455 } 456 } 457 addToList(checkExtraComponents(comp, profile.getComponents()), exList); 458 } else { 459 exList.add(new ProfileNotHL7CompliantException( 460 "A field has type primitive " + type.getClass().getName() + " but the profile defines components")); 461 } 462 } 463 464 return toArray(exList); 465 } 466 467 public HL7Exception[] testComponent(Type type, Component profile, String profileID) throws ProfileException { 468 ArrayList exList = new ArrayList(20); 469 470 addToList(testType(type, profile, null, profileID), exList); 471 472 //test children 473 if (profile.getSubComponents() > 0 && !profile.getUsage().equals("X") && hasContent(type)) { 474 if (Composite.class.isAssignableFrom(type.getClass())) { 475 Composite comp = (Composite) type; 476 for (int i = 1; i <= profile.getSubComponents(); i++) { 477 SubComponent childProfile = profile.getSubComponent(i); 478 try { 479 Type child = comp.getComponent(i-1); 480 addToList(testType(child, childProfile, null, profileID), exList); 481 } catch (DataTypeException de) { 482 exList.add(new ProfileNotHL7CompliantException("More subcomponents in profile than allowed in message: " + de.getMessage())); 483 } 484 } 485 addToList(checkExtraComponents(comp, profile.getSubComponents()), exList); 486 } else { 487 exList.add(new ProfileNotFollowedException( 488 "A component has primitive type " + type.getClass().getName() + " but the profile defines subcomponents")); 489 } 490 } 491 492 return toArray(exList); 493 } 494 495 /** Tests for extra components (ie any not defined in the profile) */ 496 private HL7Exception[] checkExtraComponents(Composite comp, int numInProfile) throws ProfileException { 497 ArrayList exList = new ArrayList(20); 498 499 StringBuffer extra = new StringBuffer(); 500 for (int i = numInProfile; i < comp.getComponents().length; i++) { 501 try { 502 String s = PipeParser.encode(comp.getComponent(i), enc); 503 if (s.length() > 0) { 504 extra.append(s); 505 extra.append(enc.getComponentSeparator()); 506 } 507 } catch (DataTypeException de) { 508 throw new ProfileException("Problem testing against profile", de); 509 } 510 } 511 512 if (extra.toString().length() > 0) { 513 exList.add(new XElementPresentException("The following components are not defined in the profile: " + extra.toString())); 514 } 515 516 return toArray(exList); 517 } 518 519 /** 520 * Tests a composite against the corresponding section of a profile. 521 */ 522 /*public HL7Exception[] testComposite(Composite comp, AbstractComposite profile) { 523 }*/ 524 525 /** 526 * Tests a primitive datatype against a profile. Tests include 527 * length, datatype, whether the profile defines any children 528 * (this would indicate an error), constant value if defined. 529 * Table values are not verified. 530 */ 531 /*public Hl7Exception[] testPrimitive(Primitive, AbstractComponent profile) { 532 533 }*/ 534 535 /** Returns true is there is content in the given structure */ 536 private boolean hasContent(Structure struct) throws HL7Exception { 537 if (Group.class.isAssignableFrom(struct.getClass())) { 538 return hasContent( (Group) struct ); 539 } else if (Segment.class.isAssignableFrom(struct.getClass())) { 540 return hasContent( (Segment) struct ); 541 } else { 542 throw new HL7Exception("Structure " + struct.getClass().getName() + " not recognized", HL7Exception.APPLICATION_INTERNAL_ERROR); 543 } 544 } 545 546 /** Returns true is there is content in the given group */ 547 private boolean hasContent(Group group) throws HL7Exception { 548 boolean has = false; 549 String encoded = PipeParser.encode(group, enc); 550 if (encoded.indexOf('|') >= 0) has = true; 551 return has; 552 } 553 554 /** Returns true is there is content in the given segment */ 555 private boolean hasContent(Segment segment) { 556 boolean has = false; 557 String encoded = PipeParser.encode(segment, enc); 558 if (encoded != null && encoded.length() > 3) has = true; 559 return has; 560 } 561 562 /** Returns true is there is content in the given type */ 563 private boolean hasContent(Type type) { 564 boolean has = false; 565 String encoded = PipeParser.encode(type, enc); 566 if (encoded != null && encoded.length() > 0) has = true; 567 return has; 568 } 569 570 /** Appends an array of HL7 exceptions to a list */ 571 private void addToList(HL7Exception[] exceptions, ArrayList list) { 572 for (int i = 0; i < exceptions.length; i++) { 573 list.add(exceptions[i]); 574 } 575 } 576 577 /** Returns the HL7 exceptions in the given arraylist in an array */ 578 private HL7Exception[] toArray(ArrayList list) { 579 return (HL7Exception[]) list.toArray(new HL7Exception[0]); 580 } 581 582 public static void main(String args[]) { 583 584 if (args.length != 2) { 585 System.out.println("Usage: DefaultValidator message_file profile_file"); 586 System.exit(1); 587 } 588 589 DefaultValidator val = new DefaultValidator(); 590 try { 591 String msgString = loadFile(args[0]); 592 Parser parser = new GenericParser(); 593 Message message = parser.parse(msgString); 594 595 String profileString = loadFile(args[1]); 596 ProfileParser profParser = new ProfileParser(true); 597 RuntimeProfile profile = profParser.parse(profileString); 598 599 HL7Exception[] exceptions = val.validate(message, profile.getMessage()); 600 601 System.out.println("Exceptions: "); 602 for (int i = 0; i < exceptions.length; i++) { 603 System.out.println((i + 1) + ". " + exceptions[i].getMessage()); 604 } 605 } catch (Exception e) { 606 e.printStackTrace(); 607 } 608 } 609 610 /** loads file at the given path */ 611 private static String loadFile(String path) throws IOException { 612 File file = new File(path); 613 //char[] cbuf = new char[(int) file.length()]; 614 BufferedReader in = new BufferedReader(new FileReader(file)); 615 StringBuffer buf = new StringBuffer(5000); 616 int c = -1; 617 while ( (c = in.read()) != -1) { 618 buf.append( (char) c ); 619 } 620 //in.read(cbuf, 0, (int) file.length()); 621 in.close(); 622 //return String.valueOf(cbuf); 623 return buf.toString(); 624 } 625 626 }