001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2025 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.parser;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.parser.json.BaseJsonLikeValue.ScalarType;
024import ca.uhn.fhir.parser.json.BaseJsonLikeValue.ValueType;
025
026import static org.apache.commons.lang3.StringUtils.isBlank;
027
028/**
029 * The default error handler, which logs issues but does not abort parsing, with only two exceptions:
030 * <p>
031 * The {@link #invalidValue(ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation, String, String)}
032 * method will throw a {@link DataFormatException} by default since ignoring this type of error
033 * can lead to data loss (since invalid values are silently ignored). See
034 * {@link #setErrorOnInvalidValue(boolean)} for information on this.
035 * </p>
036 *
037 * @see IParser#setParserErrorHandler(IParserErrorHandler)
038 * @see FhirContext#setParserErrorHandler(IParserErrorHandler)
039 *
040 * <p>
041 * The {@link #extensionContainsValueAndNestedExtensions(ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation)}
042 * method will throw a {@link DataFormatException} by default since ignoring this type of error will allow malformed
043 * resouces to be created and result in errors when attempts to read, update or delete the resource in the future.
044 *  See {@link #setErrorOnInvalidExtension(boolean)} for information on this.
045 * </p>
046 */
047public class LenientErrorHandler extends ParseErrorHandler implements IParserErrorHandler {
048
049        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LenientErrorHandler.class);
050        private static final StrictErrorHandler STRICT_ERROR_HANDLER = new StrictErrorHandler();
051        private boolean myErrorOnInvalidValue = true;
052        private boolean myErrorOnInvalidExtension = true;
053        private boolean myLogErrors;
054
055        /**
056         * Constructor which configures this handler to log all errors
057         */
058        public LenientErrorHandler() {
059                myLogErrors = true;
060        }
061
062        /**
063         * Constructor
064         *
065         * @param theLogErrors
066         *           Should errors be logged?
067         * @since 1.2
068         */
069        public LenientErrorHandler(boolean theLogErrors) {
070                myLogErrors = theLogErrors;
071        }
072
073        @Override
074        public void containedResourceWithNoId(IParseLocation theLocation) {
075                if (myLogErrors) {
076                        ourLog.warn("{}Resource has contained child resource with no ID", describeLocation(theLocation));
077                }
078        }
079
080        @Override
081        public void incorrectJsonType(
082                        IParseLocation theLocation,
083                        String theElementName,
084                        ValueType theExpected,
085                        ScalarType theExpectedScalarType,
086                        ValueType theFound,
087                        ScalarType theFoundScalarType) {
088                if (myLogErrors) {
089                        if (ourLog.isWarnEnabled()) {
090                                String message = describeLocation(theLocation)
091                                                + createIncorrectJsonTypeMessage(
092                                                                theElementName, theExpected, theExpectedScalarType, theFound, theFoundScalarType);
093                                ourLog.warn(message);
094                        }
095                }
096        }
097
098        @Override
099        public void invalidValue(IParseLocation theLocation, String theValue, String theError) {
100                if (isBlank(theValue) || myErrorOnInvalidValue == false) {
101                        if (myLogErrors) {
102                                ourLog.warn("{}Invalid attribute value \"{}\": {}", describeLocation(theLocation), theValue, theError);
103                        }
104                } else {
105                        STRICT_ERROR_HANDLER.invalidValue(theLocation, theValue, theError);
106                }
107        }
108
109        /**
110         * If set to <code>false</code> (default is <code>true</code>) invalid values will be logged. By
111         * default, invalid attribute values cause this error handler to throw a {@link DataFormatException} (unlike
112         * other methods in this class which default to simply logging errors).
113         * <p>
114         * Note that empty values (e.g. <code>""</code>) will not lead to an error when this is set to
115         * <code>true</code>, only invalid values (e.g. a gender code of <code>foo</code>)
116         * </p>
117         *
118         * @see #setErrorOnInvalidValue(boolean)
119         */
120        public boolean isErrorOnInvalidValue() {
121                return myErrorOnInvalidValue;
122        }
123
124        /**
125         * If set to <code>false</code> (default is <code>true</code>) invalid extensions will be logged. By
126         * default, invalid resource extensions cause this error handler to throw a {@link DataFormatException} (unlike
127         * other methods in this class which default to simply logging errors).
128         *
129         * @see #setErrorOnInvalidExtension(boolean)
130         */
131        public boolean isErrorOnInvalidExtension() {
132                return myErrorOnInvalidExtension;
133        }
134
135        @Override
136        public void missingRequiredElement(IParseLocation theLocation, String theElementName) {
137                if (myLogErrors) {
138                        ourLog.warn("{}Resource is missing required element: {}", describeLocation(theLocation), theElementName);
139                }
140        }
141
142        /**
143         * If set to <code>false</code> (default is <code>true</code>) invalid values will be logged. By
144         * default, invalid attribute values cause this error handler to throw a {@link DataFormatException} (unlike
145         * other methods in this class which default to simply logging errors).
146         * <p>
147         * Note that empty values (e.g. <code>""</code>) will not lead to an error when this is set to
148         * <code>true</code>, only invalid values (e.g. a gender code of <code>foo</code>)
149         * </p>
150         *
151         * @return Returns a reference to <code>this</code> for easy method chaining
152         * @see #isErrorOnInvalidValue()
153         */
154        public LenientErrorHandler setErrorOnInvalidValue(boolean theErrorOnInvalidValue) {
155                myErrorOnInvalidValue = theErrorOnInvalidValue;
156                return this;
157        }
158
159        /**
160         * If set to <code>false</code> (default is <code>true</code>) invalid extensions will be logged. By
161         * default, invalid resource extensions cause this error handler to throw a {@link DataFormatException} (unlike
162         * other methods in this class which default to simply logging errors).
163         *
164         * @return Returns a reference to <code>this</code> for easy method chaining
165         * @see #isErrorOnInvalidExtension()
166         */
167        public LenientErrorHandler setErrorOnInvalidExtension(boolean theErrorOnInvalidExtension) {
168                myErrorOnInvalidExtension = theErrorOnInvalidExtension;
169                return this;
170        }
171
172        /**
173         * If this method is called, both invalid resource extensions and invalid attribute values will set to simply logging errors.
174         */
175        public LenientErrorHandler disableAllErrors() {
176                myErrorOnInvalidValue = false;
177                myErrorOnInvalidExtension = false;
178                return this;
179        }
180
181        @Override
182        public void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName) {
183                if (myLogErrors) {
184                        ourLog.warn(
185                                        "{}Multiple repetitions of non-repeatable element '{}' found while parsing",
186                                        describeLocation(theLocation),
187                                        theElementName);
188                }
189        }
190
191        @Override
192        public void unknownAttribute(IParseLocation theLocation, String theElementName) {
193                if (myLogErrors) {
194                        ourLog.warn("{}Unknown attribute '{}' found while parsing", describeLocation(theLocation), theElementName);
195                }
196        }
197
198        @Override
199        public void unknownElement(IParseLocation theLocation, String theElementName) {
200                if (myLogErrors) {
201                        ourLog.warn("{}Unknown element '{}' found while parsing", describeLocation(theLocation), theElementName);
202                }
203        }
204
205        @Override
206        public void unknownReference(IParseLocation theLocation, String theReference) {
207                if (myLogErrors) {
208                        ourLog.warn("{}Resource has invalid reference: {}", describeLocation(theLocation), theReference);
209                }
210        }
211
212        @Override
213        public void extensionContainsValueAndNestedExtensions(IParseLocation theLocation) {
214                if (myErrorOnInvalidExtension) {
215                        STRICT_ERROR_HANDLER.extensionContainsValueAndNestedExtensions(theLocation);
216                } else if (myLogErrors) {
217                        ourLog.warn("{}Extension contains both a value and nested extensions", describeLocation(theLocation));
218                }
219        }
220
221        public static String createIncorrectJsonTypeMessage(
222                        String theElementName,
223                        ValueType theExpected,
224                        ScalarType theExpectedScalarType,
225                        ValueType theFound,
226                        ScalarType theFoundScalarType) {
227                StringBuilder b = new StringBuilder();
228                b.append("Found incorrect type for element ");
229                b.append(theElementName);
230                b.append(" - Expected ");
231                b.append(theExpected.name());
232                if (theExpectedScalarType != null) {
233                        b.append(" (");
234                        b.append(theExpectedScalarType.name());
235                        b.append(")");
236                }
237                b.append(" and found ");
238                b.append(theFound.name());
239                if (theFoundScalarType != null) {
240                        b.append(" (");
241                        b.append(theFoundScalarType.name());
242                        b.append(")");
243                }
244                String message = b.toString();
245                return message;
246        }
247}