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