Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit 4155bf2

Browse files
committed
Merge remote-tracking branch
'origin/issues/72_Validator_Generates_False-Positives' into develop
2 parents f8337ef + 2120b44 commit 4155bf2

5 files changed

Lines changed: 183 additions & 37 deletions

File tree

codex-process-data-transfer/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/data_transfer/spring/config/ValidationConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public static enum TerminologyServerConnectionTestStatus
202202
@Value("#{'${de.netzwerk.universitaetsmedizin.codex.gecco.validation.structuredefinition.modifierClasses:"
203203
+ "de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.ClosedTypeSlicingRemover,"
204204
+ "de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.MiiModuleLabObservationLab10IdentifierRemover,"
205-
+ "de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.GeccoRadiologyProceduresCodingSliceMinFixer"
205+
+ "de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.SliceMinFixer"
206206
+ "}'.trim().split('(,[ ]?)|(\\n)')}")
207207
private List<String> structureDefinitionModifierClasses;
208208

codex-process-data-transfer/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/data_transfer/validation/PluginSnapshotGeneratorWithModifiers.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,23 @@
1111
import org.springframework.beans.factory.InitializingBean;
1212

1313
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.ClosedTypeSlicingRemover;
14-
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.GeccoRadiologyProceduresCodingSliceMinFixer;
1514
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.MiiModuleLabObservationLab10IdentifierRemover;
15+
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.SliceMinFixer;
1616
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition.StructureDefinitionModifier;
1717

1818
public class PluginSnapshotGeneratorWithModifiers implements SnapshotGenerator, InitializingBean
1919
{
2020
public static final StructureDefinitionModifier CLOSED_TYPE_SLICING_REMOVER = new ClosedTypeSlicingRemover();
2121
public static final StructureDefinitionModifier MII_MODULE_LAB_OBSERVATION_LAB_1_0_IDENTIFIER_REMOVER = new MiiModuleLabObservationLab10IdentifierRemover();
22-
public static final StructureDefinitionModifier GECCO_RADIOLOGY_PROCEDURES_CODING_SLICE_MIN_FIXER = new GeccoRadiologyProceduresCodingSliceMinFixer();
22+
public static final StructureDefinitionModifier SLICE_MIN_FIXER = new SliceMinFixer();
2323

2424
private final SnapshotGenerator delegate;
2525
private final List<StructureDefinitionModifier> structureDefinitionModifiers = new ArrayList<>();
2626

2727
public PluginSnapshotGeneratorWithModifiers(SnapshotGenerator delegate)
2828
{
2929
this(delegate, Arrays.asList(CLOSED_TYPE_SLICING_REMOVER, MII_MODULE_LAB_OBSERVATION_LAB_1_0_IDENTIFIER_REMOVER,
30-
GECCO_RADIOLOGY_PROCEDURES_CODING_SLICE_MIN_FIXER));
30+
SLICE_MIN_FIXER));
3131
}
3232

3333
public PluginSnapshotGeneratorWithModifiers(SnapshotGenerator delegate,

codex-process-data-transfer/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/data_transfer/validation/structure_definition/GeccoRadiologyProceduresCodingSliceMinFixer.java

Lines changed: 0 additions & 32 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation.structure_definition;
2+
3+
import java.util.Objects;
4+
5+
import org.hl7.fhir.r4.model.StructureDefinition;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
/**
10+
* HAPI snapshot generator adds bad min value to slices if min is not explicitly defined and slicing definition is not
11+
* part of the profile and path does not end with .value[x]
12+
*/
13+
public class SliceMinFixer implements StructureDefinitionModifier
14+
{
15+
private static final Logger logger = LoggerFactory.getLogger(SliceMinFixer.class);
16+
17+
@Override
18+
public StructureDefinition modify(StructureDefinition sd)
19+
{
20+
sd.getDifferential().getElement().stream()
21+
// slice with max but no min definition
22+
.filter(e -> e.hasSliceName() && !e.hasMin() && e.hasMax())
23+
// no fix needed for rules with path ending in .value[x]
24+
.filter(e -> e.hasPath() && !e.getPath().endsWith(".value[x]"))
25+
// matching slicing definition not part of this profile (defined in base)
26+
.filter(e -> !sd.getDifferential().getElement().stream()
27+
.anyMatch(e1 -> Objects.equals(e.getPath(), e1.getPath()) && e1.hasSlicing()))
28+
.forEach(e ->
29+
{
30+
logger.warn("Adding min=0 to rule with id {} in StructureDefinition {}|{}", e.getId(), sd.getUrl(),
31+
sd.getVersion(), sd.getBaseDefinition(), sd.getDifferential().getElement().stream()
32+
.anyMatch(e1 -> Objects.equals(e.getPath(), e1.getPath()) && e1.hasSlicing()));
33+
e.setMin(0);
34+
});
35+
36+
return sd;
37+
}
38+
}

codex-process-data-transfer/src/test/java/de/netzwerk_universitaetsmedizin/codex/processes/data_transfer/validation/ValidateDataLearningTest.java

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.validation;
22

3+
import java.io.ByteArrayInputStream;
4+
import java.io.InputStream;
35
import java.nio.charset.StandardCharsets;
46
import java.nio.file.Files;
57
import java.nio.file.Path;
68
import java.nio.file.Paths;
9+
import java.security.KeyStore;
10+
import java.security.PrivateKey;
11+
import java.security.cert.Certificate;
12+
import java.security.cert.X509Certificate;
713
import java.util.Arrays;
814
import java.util.Comparator;
915
import java.util.HashSet;
16+
import java.util.List;
1017
import java.util.Map;
18+
import java.util.Properties;
1119
import java.util.Set;
20+
import java.util.UUID;
1221
import java.util.function.Consumer;
1322
import java.util.function.Function;
1423
import java.util.stream.Collectors;
@@ -27,6 +36,7 @@
2736
import org.hl7.fhir.r4.model.ElementDefinition;
2837
import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
2938
import org.hl7.fhir.r4.model.OperationOutcome;
39+
import org.hl7.fhir.r4.model.Resource;
3040
import org.hl7.fhir.r4.model.StringType;
3141
import org.hl7.fhir.r4.model.StructureDefinition;
3242
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
@@ -38,6 +48,9 @@
3848

3949
import ca.uhn.fhir.context.FhirContext;
4050
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
51+
import ca.uhn.fhir.validation.ValidationResult;
52+
import de.rwh.utils.crypto.CertificateHelper;
53+
import de.rwh.utils.crypto.io.PemIo;
4154

4255
public class ValidateDataLearningTest
4356
{
@@ -47,8 +60,20 @@ public class ValidateDataLearningTest
4760
private static final FhirContext fhirContext = FhirContext.forR4();
4861
private static final ObjectMapper mapper = ObjectMapperFactory.createObjectMapper(fhirContext);
4962

63+
private static final class ResourceAndFilename
64+
{
65+
final Resource resource;
66+
final String filename;
67+
68+
ResourceAndFilename(Resource resource, String filename)
69+
{
70+
this.resource = resource;
71+
this.filename = filename;
72+
}
73+
}
74+
5075
@Test
51-
public void testDownloadTagGz() throws Exception
76+
public void testDownloadTarGzAndParseDescriptor() throws Exception
5277
{
5378
ValidationPackageClient client = new ValidationPackageClientJersey("https://packages.simplifier.net");
5479

@@ -65,6 +90,121 @@ public void testDownloadTagGz() throws Exception
6590
descriptor.getDependencies().forEach((k, v) -> logger.debug("\t" + k + "/" + v));
6691
}
6792

93+
@Test
94+
public void testDownloadTarGzAndListExampleResources() throws Exception
95+
{
96+
ValidationPackageClient client = new ValidationPackageClientJersey("https://packages.simplifier.net");
97+
98+
ValidationPackage validationPackage = client.download("de.gecco", "1.0.5");
99+
100+
validationPackage.getEntries().forEach(e ->
101+
{
102+
if (e.getFileName() != null && e.getFileName().startsWith("package/examples/"))
103+
logger.debug(e.getFileName());
104+
});
105+
}
106+
107+
@Test
108+
public void testDownloadTarGzAndValidateExampleResources() throws Exception
109+
{
110+
ValidationPackageClient client = new ValidationPackageClientJersey("https://packages.simplifier.net");
111+
ValidationPackage validationPackage = client.download("de.gecco", "1.0.5");
112+
113+
List<ResourceAndFilename> examples = validationPackage.getEntries().stream().map(e ->
114+
{
115+
if (e.getFileName() != null && e.getFileName().startsWith("package/examples/"))
116+
{
117+
logger.debug("Reading {}", e.getFileName());
118+
try
119+
{
120+
return new ResourceAndFilename((Resource) fhirContext.newJsonParser()
121+
.parseResource(new ByteArrayInputStream(e.getContent())), e.getFileName());
122+
}
123+
catch (Exception ex)
124+
{
125+
logger.error("Error while reading {}: {}", e.getFileName(), ex.getMessage());
126+
return null;
127+
}
128+
}
129+
else
130+
return null;
131+
}).filter(e -> e != null).collect(Collectors.toList());
132+
133+
Properties properties = new Properties();
134+
try (InputStream appProperties = Files.newInputStream(Paths.get("application.properties")))
135+
{
136+
properties.load(appProperties);
137+
}
138+
139+
X509Certificate certificate = PemIo.readX509CertificateFromPem(Paths.get(properties.getProperty(
140+
"de.netzwerk.universitaetsmedizin.codex.gecco.validation.valueset.expansion.client.authentication.certificate")));
141+
char[] keyStorePassword = properties.getProperty(
142+
"de.netzwerk.universitaetsmedizin.codex.gecco.validation.valueset.expansion.client.authentication.certificate.private.key.password")
143+
.toCharArray();
144+
PrivateKey privateKey = PemIo.readPrivateKeyFromPem(Paths.get(properties.getProperty(
145+
"de.netzwerk.universitaetsmedizin.codex.gecco.validation.valueset.expansion.client.authentication.certificate.private.key")),
146+
keyStorePassword);
147+
KeyStore keyStore = CertificateHelper.toJksKeyStore(privateKey, new Certificate[] { certificate },
148+
UUID.randomUUID().toString(), keyStorePassword);
149+
150+
ValidationPackageClient validationPackageClient = new ValidationPackageClientJersey(
151+
"https://packages.simplifier.net");
152+
ValidationPackageClient validationPackageClientWithCache = new ValidationPackageClientWithFileSystemCache(
153+
cacheFolder, mapper, validationPackageClient);
154+
ValueSetExpansionClient valueSetExpansionClient = new ValueSetExpansionClientJersey(
155+
"https://terminology-highmed.medic.medfak.uni-koeln.de/fhir", null, keyStore, keyStorePassword, null,
156+
null, null, null, null, 0, 0, false, mapper, fhirContext);
157+
ValueSetExpansionClient valueSetExpansionClientWithCache = new ValueSetExpansionClientWithFileSystemCache(
158+
cacheFolder, fhirContext, valueSetExpansionClient);
159+
ValidationPackageManager manager = new ValidationPackageManagerImpl(validationPackageClientWithCache,
160+
valueSetExpansionClientWithCache, mapper, fhirContext,
161+
(fc, vs) -> new PluginSnapshotGeneratorWithFileSystemCache(cacheFolder, fc,
162+
new PluginSnapshotGeneratorWithModifiers(new PluginSnapshotGeneratorImpl(fc, vs))),
163+
(fc, vs) -> new ValueSetExpanderWithFileSystemCache(cacheFolder, fc, new ValueSetExpanderImpl(fc, vs)));
164+
165+
BundleValidator validator = manager.createBundleValidator("de.gecco", "1.0.5");
166+
167+
examples.forEach(r ->
168+
{
169+
ValidationResult result;
170+
try
171+
{
172+
logger.debug("Validating resource of type {} from {}", r.resource.getResourceType().name(), r.filename);
173+
result = validator.validate(r.resource);
174+
}
175+
catch (Exception ex)
176+
{
177+
logger.error("Unable to validate resource of type {} from {}: {}", r.resource.getResourceType().name(),
178+
r.filename, ex.getMessage());
179+
return;
180+
}
181+
182+
OperationOutcome outcome = (OperationOutcome) result.toOperationOutcome();
183+
184+
outcome.getIssue().forEach(issue ->
185+
{
186+
if (OperationOutcome.IssueSeverity.FATAL.equals(issue.getSeverity()))
187+
logger.error("Bundle fatal validation error ({}): {}",
188+
issue.getLocation().stream().map(StringType::getValue).collect(Collectors.joining(", ")),
189+
issue.getDiagnostics());
190+
else if (OperationOutcome.IssueSeverity.ERROR.equals(issue.getSeverity()))
191+
logger.error("Bundle validation error ({}): {}",
192+
issue.getLocation().stream().map(StringType::getValue).collect(Collectors.joining(", ")),
193+
issue.getDiagnostics());
194+
else if (OperationOutcome.IssueSeverity.WARNING.equals(issue.getSeverity()))
195+
logger.warn("Bundle validation warning ({}): {}",
196+
issue.getLocation().stream().map(StringType::getValue).collect(Collectors.joining(", ")),
197+
issue.getDiagnostics());
198+
else if (issue.hasLocation())
199+
logger.info("Bundle validation info ({}): {}",
200+
issue.getLocation().stream().map(StringType::getValue).collect(Collectors.joining(", ")),
201+
issue.getDiagnostics());
202+
else
203+
logger.info("Bundle validation info: {}", issue.getDiagnostics());
204+
});
205+
});
206+
}
207+
68208
@Test
69209
public void testDownloadWithDependencies() throws Exception
70210
{

0 commit comments

Comments
 (0)