1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package ca.uhn.hl7v2.conf.check;
29
30 import java.io.BufferedReader;
31 import java.io.File;
32 import java.io.FileReader;
33 import java.io.IOException;
34 import java.util.ArrayList;
35 import java.util.List;
36
37 import ca.uhn.hl7v2.model.*;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import ca.uhn.hl7v2.DefaultHapiContext;
42 import ca.uhn.hl7v2.HL7Exception;
43 import ca.uhn.hl7v2.HapiContext;
44 import ca.uhn.hl7v2.HapiContextSupport;
45 import ca.uhn.hl7v2.conf.ProfileException;
46 import ca.uhn.hl7v2.conf.parser.ProfileParser;
47 import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
48 import ca.uhn.hl7v2.conf.spec.message.AbstractComponent;
49 import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer;
50 import ca.uhn.hl7v2.conf.spec.message.Component;
51 import ca.uhn.hl7v2.conf.spec.message.Field;
52 import ca.uhn.hl7v2.conf.spec.message.ProfileStructure;
53 import ca.uhn.hl7v2.conf.spec.message.Seg;
54 import ca.uhn.hl7v2.conf.spec.message.SegGroup;
55 import ca.uhn.hl7v2.conf.spec.message.StaticDef;
56 import ca.uhn.hl7v2.conf.spec.message.SubComponent;
57 import ca.uhn.hl7v2.conf.store.CodeStore;
58 import ca.uhn.hl7v2.conf.store.ProfileStoreFactory;
59 import ca.uhn.hl7v2.parser.EncodingCharacters;
60 import ca.uhn.hl7v2.parser.GenericParser;
61 import ca.uhn.hl7v2.parser.Parser;
62 import ca.uhn.hl7v2.parser.PipeParser;
63 import ca.uhn.hl7v2.util.Terser;
64
65
66
67
68
69
70
71
72 public class DefaultValidator extends HapiContextSupport implements Validator {
73
74 private final EncodingCharacters enc;
75 private static final Logger log = LoggerFactory.getLogger(DefaultValidator.class);
76 private boolean validateChildren = true;
77 private CodeStore codeStore;
78
79
80 public DefaultValidator() {
81 this(new DefaultHapiContext());
82 }
83
84 public DefaultValidator(HapiContext context) {
85 super(context);
86 enc = new EncodingCharacters('|', null);
87 }
88
89
90
91
92
93 public void setValidateChildren(boolean validateChildren) {
94 this.validateChildren = validateChildren;
95 }
96
97
98
99
100
101
102
103
104
105 public void setCodeStore(CodeStore theCodeStore) {
106 codeStore = theCodeStore;
107 }
108
109
110
111
112 public HL7Exception[] validate(Message message, StaticDef profile) throws ProfileException,
113 HL7Exception {
114 List<HL7Exception> exList = new ArrayList<>();
115 Terser t = new Terser(message);
116
117 checkMessageType(t.get("/MSH-9-1"), profile, exList);
118 checkEventType(t.get("/MSH-9-2"), profile, exList);
119 checkMessageStructure(t.get("/MSH-9-3"), profile, exList);
120
121 exList.addAll(doTestGroup(message, profile, profile.getIdentifier(),
122 validateChildren));
123 return exList.toArray(new HL7Exception[0]);
124 }
125
126
127 protected void checkEventType(String evType, StaticDef profile, List<HL7Exception> exList) {
128 if (!evType.equals(profile.getEventType())
129 && !profile.getEventType().equalsIgnoreCase("ALL")) {
130 HL7Exception e = new ProfileNotFollowedException("Event type " + evType
131 + " doesn't match profile type of " + profile.getEventType());
132 exList.add(e);
133 }
134 }
135
136 protected void checkMessageType(String msgType, StaticDef profile, List<HL7Exception> exList) {
137 if (!msgType.equals(profile.getMsgType())) {
138 HL7Exception e = new ProfileNotFollowedException("Message type " + msgType
139 + " doesn't match profile type of " + profile.getMsgType());
140 exList.add(e);
141 }
142 }
143
144 protected void checkMessageStructure(String msgStruct, StaticDef profile, List<HL7Exception> exList) {
145 if (msgStruct == null || !msgStruct.equals(profile.getMsgStructID())) {
146 HL7Exception e = new ProfileNotFollowedException("Message structure " + msgStruct
147 + " doesn't match profile type of " + profile.getMsgStructID());
148 exList.add(e);
149 }
150 }
151
152
153
154
155 public List<HL7Exception> testGroup(Group group, SegGroup profile, String profileID)
156 throws ProfileException {
157 return doTestGroup(group, profile, profileID, true);
158 }
159
160 protected List<HL7Exception> doTestGroup(Group group, AbstractSegmentContainer profile,
161 String profileID, boolean theValidateChildren) throws ProfileException {
162 List<HL7Exception> exList = new ArrayList<>();
163 List<String> allowedStructures = new ArrayList<>();
164
165 for (ProfileStructure struct : profile) {
166
167
168 if (!struct.getUsage().equalsIgnoreCase("X")) {
169 allowedStructures.add(struct.getName());
170
171
172 try {
173 List<Structure> instancesWithContent = new ArrayList<>();
174 for (Structure instance : group.getAll(struct.getName())) {
175 if (!instance.isEmpty())
176 instancesWithContent.add(instance);
177 }
178
179 testCardinality(instancesWithContent.size(), struct.getMin(),
180 struct.getMax(), struct.getUsage(), struct.getName(), exList);
181
182
183 if (theValidateChildren) {
184 for (Structure s : instancesWithContent) {
185 exList.addAll(testStructure(s, struct, profileID));
186 }
187 }
188
189 } catch (HL7Exception he) {
190 exList.add(new ProfileNotHL7CompliantException(struct.getName()
191 + " not found in message"));
192 }
193 }
194 }
195
196
197 checkForExtraStructures(group, allowedStructures, exList);
198
199 return exList;
200 }
201
202
203
204
205
206
207 protected void checkForExtraStructures(Group group, List<String> allowedStructures, List<HL7Exception> exList)
208 throws ProfileException {
209 for (String childName : group.getNames()) {
210 if (!allowedStructures.contains(childName)) {
211 try {
212 for (Structure rep : group.getAll(childName)) {
213 if (!rep.isEmpty()) {
214 HL7Exception e = new XElementPresentException("The structure "
215 + childName + " appears in the message but not in the profile");
216 exList.add(e);
217 }
218 }
219 } catch (HL7Exception he) {
220 throw new ProfileException("Problem checking profile", he);
221 }
222 }
223 }
224 }
225
226
227
228
229
230
231
232
233
234
235
236
237
238 protected HL7Exception testCardinality(int reps, int min, int max, String usage, String name, List<HL7Exception> exList) {
239 HL7Exception e = null;
240 if (reps < min && usage.equalsIgnoreCase("R")) {
241 e = new ProfileNotFollowedException(name + " must have at least " + min
242 + " repetitions (has " + reps + ")");
243 } else if (max > 0 && reps > max) {
244 e = new ProfileNotFollowedException(name + " must have no more than " + max
245 + " repetitions (has " + reps + ")");
246 }
247 if (e != null) exList.add(e);
248 return e;
249 }
250
251
252
253
254 public List<HL7Exception> testStructure(Structure s, ProfileStructure profile, String profileID)
255 throws ProfileException {
256 List<HL7Exception> exList = new ArrayList<>();
257 if (profile instanceof Seg) {
258 if (Segment.class.isAssignableFrom(s.getClass())) {
259 exList.addAll(doTestSegment((Segment) s, (Seg) profile, profileID, validateChildren));
260 } else {
261 exList.add(new ProfileNotHL7CompliantException(
262 "Mismatch between a segment in the profile and the structure "
263 + s.getClass().getName() + " in the message"));
264 }
265 } else if (profile instanceof SegGroup) {
266 if (Group.class.isAssignableFrom(s.getClass())) {
267 exList.addAll(testGroup((Group) s, (SegGroup) profile, profileID));
268 } else {
269 exList.add(new ProfileNotHL7CompliantException(
270 "Mismatch between a group in the profile and the structure "
271 + s.getClass().getName() + " in the message"));
272 }
273 }
274 return exList;
275 }
276
277
278
279
280 public List<HL7Exception> testSegment(ca.uhn.hl7v2.model.Segment segment, Seg profile,
281 String profileID) throws ProfileException {
282 return doTestSegment(segment, profile, profileID, true);
283 }
284
285 protected List<HL7Exception> doTestSegment(ca.uhn.hl7v2.model.Segment segment, Seg profile,
286 String profileID, boolean theValidateChildren) throws ProfileException {
287 List<HL7Exception> exList = new ArrayList<>();
288 List<Integer> allowedFields = new ArrayList<>();
289
290 for (int i = 1; i <= profile.getFields(); i++) {
291 Field field = profile.getField(i);
292
293
294 if (!field.getUsage().equalsIgnoreCase("X")) {
295 allowedFields.add(i);
296
297
298 try {
299 Type[] instances = segment.getField(i);
300 List<Type> instancesWithContent = new ArrayList<>();
301 for (Type instance : instances) {
302 if (!instance.isEmpty())
303 instancesWithContent.add(instance);
304 }
305
306 HL7Exception ce = testCardinality(instancesWithContent.size(), field.getMin(),
307 field.getMax(), field.getUsage(), field.getName(), exList);
308 if (ce != null) {
309 ce.setFieldPosition(i);
310 }
311
312
313 if (theValidateChildren) {
314 for (Type s : instancesWithContent) {
315
316 boolean escape = !(profile.getName().equalsIgnoreCase("MSH") && i < 3);
317 List<HL7Exception> childExceptions = doTestField(s, field, escape,
318 profileID, validateChildren);
319 for (HL7Exception ex : childExceptions) {
320 ex.setFieldPosition(i);
321 }
322 exList.addAll(childExceptions);
323 }
324 }
325
326 } catch (HL7Exception he) {
327 exList.add(new ProfileNotHL7CompliantException("Field " + i
328 + " not found in message"));
329 }
330 }
331
332 }
333
334
335 checkForExtraFields(segment, allowedFields, exList);
336
337 for (HL7Exception ex : exList) {
338 ex.setSegmentName(profile.getName());
339 }
340 return exList;
341 }
342
343
344
345
346
347
348
349
350 protected void checkForExtraFields(Segment segment, List<Integer> allowedFields, List<HL7Exception> exList)
351 throws ProfileException {
352 for (int i = 1; i <= segment.numFields(); i++) {
353 if (!allowedFields.contains(i)) {
354 try {
355 Type[] reps = segment.getField(i);
356 for (Type rep : reps) {
357 if (!rep.isEmpty()) {
358 HL7Exception e = new XElementPresentException("Field " + i + " in "
359 + segment.getName()
360 + " appears in the message but not in the profile");
361 exList.add(e);
362 }
363 }
364 } catch (HL7Exception he) {
365 throw new ProfileException("Problem testing against profile", he);
366 }
367 }
368 }
369 }
370
371
372
373
374
375
376
377 public List<HL7Exception> testType(Type type, AbstractComponent<?> profile, String encoded,
378 String profileID) {
379 List<HL7Exception> exList = new ArrayList<>();
380 if (encoded == null)
381 encoded = PipeParser.encode(type, this.enc);
382
383 testUsage(encoded, profile.getUsage(), profile.getName(), exList);
384
385 if (!profile.getUsage().equals("X")) {
386 checkDataType(profile.getDatatype(), type, exList);
387 checkLength(profile.getLength(), profile.getName(), encoded, exList);
388 checkConstantValue(profile.getConstantValue(), encoded, exList);
389
390 testTypeAgainstTable(type, profile, profileID, exList);
391 }
392
393 return exList;
394 }
395
396 protected void checkConstantValue(String value, String encoded, List<HL7Exception> exList) {
397
398 if (value != null && value.length() > 0) {
399 if (!encoded.equals(value))
400 exList.add(new ProfileNotFollowedException("'" + encoded
401 + "' doesn't equal constant value of '" + value + "'"));
402 }
403 }
404
405 protected void checkLength(long length, String name, String encoded, List<HL7Exception> exList) {
406
407 if (encoded.length() > length)
408 exList.add(new ProfileNotFollowedException("The type " + name
409 + " has length " + encoded.length() + " which exceeds max of "
410 + length));
411 }
412
413 protected void checkDataType(String dataType, Type type, List<HL7Exception> exList) {
414
415 String typeName = type.getName();
416 if (!(type instanceof Varies || typeName.equals(dataType))) {
417 exList.add(new ProfileNotHL7CompliantException("HL7 datatype " + typeName
418 + " doesn't match profile datatype " + dataType));
419 }
420 }
421
422
423
424
425
426
427 public HL7Exception testLength(Type type, int maxLength) {
428 HL7Exception e = null;
429 String encoded = PipeParser.encode(type, this.enc);
430 if (encoded.length() > maxLength) {
431 e = new ProfileNotFollowedException("Length of " + encoded.length()
432 + " exceeds maximum of " + maxLength);
433 }
434 return e;
435 }
436
437
438
439
440
441
442
443
444
445
446 protected void testUsage(String encoded, String usage, String name, List<HL7Exception> exList) {
447 if (usage.equalsIgnoreCase("R")) {
448 if (encoded.length() == 0)
449 exList.add(new ProfileNotFollowedException("Required element " + name + " is missing"));
450 } else if (usage.equalsIgnoreCase("RE")) {
451
452 } else if (usage.equalsIgnoreCase("O")) {
453
454 } else if (usage.equalsIgnoreCase("C")) {
455
456 } else if (usage.equalsIgnoreCase("CE")) {
457
458 } else if (usage.equalsIgnoreCase("X")) {
459 if (encoded.length() > 0)
460 exList.add(new XElementPresentException("Element \"" + name
461 + "\" is present but specified as not used (X)"));
462 } else if (usage.equalsIgnoreCase("B")) {
463
464 }
465 }
466
467
468
469
470
471 protected void testTypeAgainstTable(Type type, AbstractComponent<?> profile,
472 String profileID, List<HL7Exception> exList) {
473 if (profile.getTable() != null
474 && (type.getName().equals("IS") || type.getName().equals("ID"))) {
475 String codeSystem = String.format("HL7%1$4s", profile.getTable()).replace(" ", "0");
476 String value = ((Primitive) type).getValue();
477 addTableTestResult(profileID, codeSystem, value, exList);
478 } else if (type.getName().equals("CE")) {
479 String value = Terser.getPrimitive(type, 1, 1).getValue();
480 String codeSystem = Terser.getPrimitive(type, 3, 1).getValue();
481 addTableTestResult(profileID, codeSystem, value, exList);
482
483 value = Terser.getPrimitive(type, 4, 1).getValue();
484 codeSystem = Terser.getPrimitive(type, 6, 1).getValue();
485 addTableTestResult(profileID, codeSystem, value, exList);
486 }
487 }
488
489 protected void addTableTestResult(String profileID, String codeSystem, String value, List<HL7Exception> exList) {
490 if (codeSystem != null && value != null && validateChildren) {
491 testValueAgainstTable(profileID, codeSystem, value, exList);
492 }
493 }
494
495 protected void testValueAgainstTable(String profileID, String codeSystem, String value, List<HL7Exception> exList) {
496 CodeStore store = codeStore;
497 if (codeStore == null) {
498 store = getHapiContext().getCodeStoreRegistry().getCodeStore(profileID, codeSystem);
499 }
500
501 if (store == null) {
502 log.info(
503 "Not checking value {}: no code store was found for profile {} code system {}",
504 value, profileID, codeSystem);
505 } else {
506 if (!store.knowsCodes(codeSystem)) {
507 log.warn("Not checking value {}: Don't have a table for code system {}", value,
508 codeSystem);
509 } else if (!store.isValidCode(codeSystem, value)) {
510 exList.add(new ProfileNotFollowedException("Code '" + value + "' not found in table "
511 + codeSystem + ", profile " + profileID));
512 }
513 }
514
515 }
516
517 public List<HL7Exception> testField(Type type, Field profile, boolean escape, String profileID)
518 throws ProfileException, HL7Exception {
519 return doTestField(type, profile, escape, profileID, true);
520 }
521
522 protected List<HL7Exception> doTestField(Type type, Field profile, boolean escape, String profileID,
523 boolean theValidateChildren) throws ProfileException, HL7Exception {
524
525
526 String encoded = null;
527 if (!escape && Primitive.class.isAssignableFrom(type.getClass()))
528 encoded = ((Primitive) type).getValue();
529
530 List<HL7Exception> exList = new ArrayList<>(testType(type, profile, encoded, profileID));
531
532
533 if (theValidateChildren) {
534 if (profile.getComponents() > 0 && !profile.getUsage().equals("X")) {
535 if (Composite.class.isAssignableFrom(type.getClass())) {
536 Composite comp = (Composite) type;
537 for (int i = 1; i <= profile.getComponents(); i++) {
538 Component childProfile = profile.getComponent(i);
539 try {
540 Type child = comp.getComponent(i - 1);
541 exList.addAll(doTestComponent(child, childProfile, profileID, validateChildren));
542 } catch (DataTypeException de) {
543 exList.add(new ProfileNotHL7CompliantException(
544 "More components in profile than allowed in message: "
545 + de.getMessage()));
546 }
547 }
548 checkExtraComponents(comp, profile.getComponents(), exList);
549 } else {
550 exList.add(new ProfileNotHL7CompliantException("A field has type primitive "
551 + type.getClass().getName() + " but the profile defines components"));
552 }
553 }
554 }
555
556 return exList;
557 }
558
559 public List<HL7Exception> testComponent(Type type, Component profile, String profileID)
560 throws ProfileException, HL7Exception {
561 return doTestComponent(type, profile, profileID, true);
562 }
563
564 protected List<HL7Exception> doTestComponent(Type type, Component profile, String profileID,
565 boolean theValidateChildren) throws ProfileException, HL7Exception {
566 List<HL7Exception> exList = new ArrayList<>(testType(type, profile, null, profileID));
567
568
569 if (profile.getSubComponents() > 0 && !profile.getUsage().equals("X") && (!type.isEmpty())) {
570 if (Composite.class.isAssignableFrom(type.getClass())) {
571 Composite comp = (Composite) type;
572
573 if (theValidateChildren) {
574 for (int i = 1; i <= profile.getSubComponents(); i++) {
575 SubComponent childProfile = profile.getSubComponent(i);
576 try {
577 Type child = comp.getComponent(i - 1);
578 exList.addAll(testType(child, childProfile, null, profileID));
579 } catch (DataTypeException de) {
580 exList.add(new ProfileNotHL7CompliantException(
581 "More subcomponents in profile than allowed in message: "
582 + de.getMessage()));
583 }
584 }
585 }
586
587 checkExtraComponents(comp, profile.getSubComponents(), exList);
588 } else {
589 exList.add(new ProfileNotFollowedException("A component has primitive type "
590 + type.getClass().getName() + " but the profile defines subcomponents"));
591 }
592 }
593
594 return exList;
595 }
596
597
598 protected void checkExtraComponents(Composite comp, int numInProfile, List<HL7Exception> exList)
599 throws ProfileException {
600 StringBuilder extra = new StringBuilder();
601 for (int i = numInProfile; i < comp.getComponents().length; i++) {
602 try {
603 String s = PipeParser.encode(comp.getComponent(i), enc);
604 if (s.length() > 0) {
605 extra.append(s).append(enc.getComponentSeparator());
606 }
607 } catch (DataTypeException de) {
608 throw new ProfileException("Problem testing against profile", de);
609 }
610 }
611
612 if (extra.length() > 0) {
613 exList.add(new XElementPresentException(
614 "The following components are not defined in the profile: " + extra.toString()));
615 }
616
617 }
618
619 public static void main(String[] args) {
620
621 if (args.length != 2) {
622 System.out.println("Usage: DefaultValidator message_file profile_file");
623 System.exit(1);
624 }
625
626 DefaultValidator val = new DefaultValidator();
627 try {
628 String msgString = loadFile(args[0]);
629 Parser parser = new GenericParser();
630 Message message = parser.parse(msgString);
631
632 String profileString = loadFile(args[1]);
633 ProfileParser profParser = new ProfileParser(true);
634 RuntimeProfile profile = profParser.parse(profileString);
635
636 HL7Exception[] exceptions = val.validate(message, profile.getMessage());
637
638 System.out.println("Exceptions: ");
639 for (int i = 0; i < exceptions.length; i++) {
640 System.out.println((i + 1) + ". " + exceptions[i].getMessage());
641 }
642 } catch (Exception e) {
643 e.printStackTrace();
644 }
645 }
646
647
648 private static String loadFile(String path) throws IOException {
649 File file = new File(path);
650
651 BufferedReader in = new BufferedReader(new FileReader(file));
652 StringBuilder buf = new StringBuilder(5000);
653 int c;
654 while ((c = in.read()) != -1) {
655 buf.append((char) c);
656 }
657
658 in.close();
659
660 return buf.toString();
661 }
662
663 }