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.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");
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 = createIncorrectJsonTypeMessage(
091                                                theElementName, theExpected, theExpectedScalarType, theFound, theFoundScalarType);
092                                ourLog.warn(message);
093                        }
094                }
095        }
096
097        @Override
098        public void invalidValue(IParseLocation theLocation, String theValue, String theError) {
099                if (isBlank(theValue) || myErrorOnInvalidValue == false) {
100                        if (myLogErrors) {
101                                ourLog.warn("{}Invalid attribute value \"{}\": {}", describeLocation(theLocation), theValue, theError);
102                        }
103                } else {
104                        STRICT_ERROR_HANDLER.invalidValue(theLocation, theValue, theError);
105                }
106        }
107
108        /**
109         * If set to <code>false</code> (default is <code>true</code>) invalid values will be logged. By
110         * default, invalid attribute values cause this error handler to throw a {@link DataFormatException} (unlike
111         * other methods in this class which default to simply logging errors).
112         * <p>
113         * Note that empty values (e.g. <code>""</code>) will not lead to an error when this is set to
114         * <code>true</code>, only invalid values (e.g. a gender code of <code>foo</code>)
115         * </p>
116         *
117         * @see #setErrorOnInvalidValue(boolean)
118         */
119        public boolean isErrorOnInvalidValue() {
120                return myErrorOnInvalidValue;
121        }
122
123        /**
124         * If set to <code>false</code> (default is <code>true</code>) invalid extensions will be logged. By
125         * default, invalid resource extensions cause this error handler to throw a {@link DataFormatException} (unlike
126         * other methods in this class which default to simply logging errors).
127         *
128         * @see #setErrorOnInvalidExtension(boolean)
129         */
130        public boolean isErrorOnInvalidExtension() {
131                return myErrorOnInvalidExtension;
132        }
133
134        @Override
135        public void missingRequiredElement(IParseLocation theLocation, String theElementName) {
136                if (myLogErrors) {
137                        ourLog.warn("Resource is missing required element: {}", theElementName);
138                }
139        }
140
141        /**
142         * If set to <code>false</code> (default is <code>true</code>) invalid values will be logged. By
143         * default, invalid attribute values cause this error handler to throw a {@link DataFormatException} (unlike
144         * other methods in this class which default to simply logging errors).
145         * <p>
146         * Note that empty values (e.g. <code>""</code>) will not lead to an error when this is set to
147         * <code>true</code>, only invalid values (e.g. a gender code of <code>foo</code>)
148         * </p>
149         *
150         * @return Returns a reference to <code>this</code> for easy method chaining
151         * @see #isErrorOnInvalidValue()
152         */
153        public LenientErrorHandler setErrorOnInvalidValue(boolean theErrorOnInvalidValue) {
154                myErrorOnInvalidValue = theErrorOnInvalidValue;
155                return this;
156        }
157
158        /**
159         * If set to <code>false</code> (default is <code>true</code>) invalid extensions will be logged. By
160         * default, invalid resource extensions cause this error handler to throw a {@link DataFormatException} (unlike
161         * other methods in this class which default to simply logging errors).
162         *
163         * @return Returns a reference to <code>this</code> for easy method chaining
164         * @see #isErrorOnInvalidExtension()
165         */
166        public LenientErrorHandler setErrorOnInvalidExtension(boolean theErrorOnInvalidExtension) {
167                myErrorOnInvalidExtension = theErrorOnInvalidExtension;
168                return this;
169        }
170
171        /**
172         * If this method is called, both invalid resource extensions and invalid attribute values will set to simply logging errors.
173         */
174        public LenientErrorHandler disableAllErrors() {
175                myErrorOnInvalidValue = false;
176                myErrorOnInvalidExtension = false;
177                return this;
178        }
179
180        @Override
181        public void unexpectedRepeatingElement(IParseLocation theLocation, String theElementName) {
182                if (myLogErrors) {
183                        ourLog.warn(
184                                        "{}Multiple repetitions of non-repeatable element '{}' found while parsing",
185                                        describeLocation(theLocation),
186                                        theElementName);
187                }
188        }
189
190        @Override
191        public void unknownAttribute(IParseLocation theLocation, String theElementName) {
192                if (myLogErrors) {
193                        ourLog.warn("{}Unknown attribute '{}' found while parsing", describeLocation(theLocation), theElementName);
194                }
195        }
196
197        @Override
198        public void unknownElement(IParseLocation theLocation, String theElementName) {
199                if (myLogErrors) {
200                        ourLog.warn("{}Unknown element '{}' found while parsing", describeLocation(theLocation), theElementName);
201                }
202        }
203
204        @Override
205        public void unknownReference(IParseLocation theLocation, String theReference) {
206                if (myLogErrors) {
207                        ourLog.warn("{}Resource has invalid reference: {}", describeLocation(theLocation), theReference);
208                }
209        }
210
211        @Override
212        public void extensionContainsValueAndNestedExtensions(IParseLocation theLocation) {
213                if (myErrorOnInvalidExtension) {
214                        STRICT_ERROR_HANDLER.extensionContainsValueAndNestedExtensions(theLocation);
215                } else if (myLogErrors) {
216                        ourLog.warn("{}Extension contains both a value and nested extensions", describeLocation(theLocation));
217                }
218        }
219
220        public static String createIncorrectJsonTypeMessage(
221                        String theElementName,
222                        ValueType theExpected,
223                        ScalarType theExpectedScalarType,
224                        ValueType theFound,
225                        ScalarType theFoundScalarType) {
226                StringBuilder b = new StringBuilder();
227                b.append("Found incorrect type for element ");
228                b.append(theElementName);
229                b.append(" - Expected ");
230                b.append(theExpected.name());
231                if (theExpectedScalarType != null) {
232                        b.append(" (");
233                        b.append(theExpectedScalarType.name());
234                        b.append(")");
235                }
236                b.append(" and found ");
237                b.append(theFound.name());
238                if (theFoundScalarType != null) {
239                        b.append(" (");
240                        b.append(theFoundScalarType.name());
241                        b.append(")");
242                }
243                String message = b.toString();
244                return message;
245        }
246}