001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.validation.schematron;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.rest.api.EncodingEnum;
025import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
026import ca.uhn.fhir.util.BundleUtil;
027import ca.uhn.fhir.validation.FhirValidator;
028import ca.uhn.fhir.validation.IValidationContext;
029import ca.uhn.fhir.validation.IValidatorModule;
030import ca.uhn.fhir.validation.ResultSeverityEnum;
031import ca.uhn.fhir.validation.SchemaBaseValidator;
032import ca.uhn.fhir.validation.SingleValidationMessage;
033import ca.uhn.fhir.validation.ValidationContext;
034import com.helger.commons.error.IError;
035import com.helger.commons.error.list.IErrorList;
036import com.helger.commons.io.resource.ClassPathResource;
037import com.helger.commons.io.resource.IReadableResource;
038import com.helger.schematron.ISchematronResource;
039import com.helger.schematron.SchematronHelper;
040import com.helger.schematron.sch.SchematronResourceSCH;
041import com.helger.schematron.svrl.jaxb.SchematronOutputType;
042import org.hl7.fhir.instance.model.api.IBaseBundle;
043import org.hl7.fhir.instance.model.api.IBaseResource;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import java.io.IOException;
048import java.io.InputStream;
049import java.io.StringReader;
050import java.util.HashMap;
051import java.util.List;
052import java.util.Locale;
053import java.util.Map;
054import javax.xml.transform.stream.StreamSource;
055
056/**
057 * This class is only used using reflection from {@link SchematronProvider} in order
058 * to be truly optional.
059 */
060public class SchematronBaseValidator implements IValidatorModule {
061
062        private static final Logger ourLog = LoggerFactory.getLogger(SchematronBaseValidator.class);
063        private final Map<Class<? extends IBaseResource>, ISchematronResource> myClassToSchematron = new HashMap<>();
064        private FhirContext myCtx;
065
066        /**
067         * Constructor
068         */
069        public SchematronBaseValidator(FhirContext theContext) {
070                myCtx = theContext;
071        }
072
073        @Override
074        public void validateResource(IValidationContext<IBaseResource> theCtx) {
075
076                if (theCtx.getResource() instanceof IBaseBundle) {
077                        IBaseBundle bundle = (IBaseBundle) theCtx.getResource();
078                        List<IBaseResource> subResources = BundleUtil.toListOfResources(myCtx, bundle);
079                        for (IBaseResource nextSubResource : subResources) {
080                                validateResource(ValidationContext.subContext(theCtx, nextSubResource, theCtx.getOptions()));
081                        }
082                }
083
084                ISchematronResource sch = getSchematron(theCtx);
085                String resourceAsString;
086                if (theCtx.getResourceAsStringEncoding() == EncodingEnum.XML) {
087                        resourceAsString = theCtx.getResourceAsString();
088                } else {
089                        resourceAsString = theCtx.getFhirContext().newXmlParser().encodeResourceToString(theCtx.getResource());
090                }
091                StreamSource source = new StreamSource(new StringReader(resourceAsString));
092
093                SchematronOutputType results;
094                try {
095                        results = sch.applySchematronValidationToSVRL(source);
096                } catch (Exception e) {
097                        throw new InternalErrorException(Msg.code(2433) + e.getMessage(), e);
098                }
099                if (results == null) {
100                        return;
101                }
102
103                IErrorList errors = SchematronHelper.convertToErrorList(
104                                results,
105                                theCtx.getFhirContext()
106                                                .getResourceDefinition(theCtx.getResource())
107                                                .getBaseDefinition()
108                                                .getName());
109
110                if (errors.getAllErrors().containsOnlySuccess()) {
111                        return;
112                }
113
114                for (IError next : errors) {
115                        ResultSeverityEnum severity;
116                        if (next.isFailure()) {
117                                severity = ResultSeverityEnum.ERROR;
118                        } else if (next.isError()) {
119                                severity = ResultSeverityEnum.FATAL;
120                        } else if (next.isNoError()) {
121                                severity = ResultSeverityEnum.WARNING;
122                        } else {
123                                continue;
124                        }
125
126                        String details = next.getAsString(Locale.getDefault());
127
128                        SingleValidationMessage message = new SingleValidationMessage();
129                        message.setMessage(details);
130                        message.setLocationLine(next.getErrorLocation().getLineNumber());
131                        message.setLocationCol(next.getErrorLocation().getColumnNumber());
132                        message.setLocationString(next.getErrorLocation().getAsString());
133                        message.setSeverity(severity);
134                        theCtx.addValidationMessage(message);
135                }
136        }
137
138        private ISchematronResource getSchematron(IValidationContext<IBaseResource> theCtx) {
139                Class<? extends IBaseResource> resource = theCtx.getResource().getClass();
140                Class<? extends IBaseResource> baseResourceClass = theCtx.getFhirContext()
141                                .getResourceDefinition(resource)
142                                .getBaseDefinition()
143                                .getImplementingClass();
144
145                return getSchematronAndCache(theCtx, baseResourceClass);
146        }
147
148        private ISchematronResource getSchematronAndCache(
149                        IValidationContext<IBaseResource> theCtx, Class<? extends IBaseResource> theClass) {
150                synchronized (myClassToSchematron) {
151                        ISchematronResource retVal = myClassToSchematron.get(theClass);
152                        if (retVal != null) {
153                                return retVal;
154                        }
155
156                        String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/'
157                                        + theCtx.getFhirContext()
158                                                        .getResourceDefinition(theCtx.getResource())
159                                                        .getBaseDefinition()
160                                                        .getName()
161                                                        .toLowerCase()
162                                        + ".sch";
163                        try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) {
164                                if (baseIs == null) {
165                                        throw new InternalErrorException(Msg.code(1972) + "Failed to load schematron for resource '"
166                                                        + theCtx.getFhirContext()
167                                                                        .getResourceDefinition(theCtx.getResource())
168                                                                        .getBaseDefinition()
169                                                                        .getName()
170                                                        + "'. " + SchemaBaseValidator.RESOURCES_JAR_NOTE);
171                                }
172                        } catch (IOException e) {
173                                ourLog.error("Failed to close stream", e);
174                        }
175
176                        // Allow Schematron to load SCH files from the 'validation-resources'
177                        // bundles when running in an OSGi container. This is because the
178                        // Schematron bundle does not have DynamicImport-Package in its manifest.
179                        IReadableResource schResource =
180                                        new ClassPathResource(pathToBase, this.getClass().getClassLoader());
181                        retVal = new SchematronResourceSCH(schResource);
182                        myClassToSchematron.put(theClass, retVal);
183                        return retVal;
184                }
185        }
186}