
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2023 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.jpa.dao; 021 022import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.parser.DataFormatException; 025import ca.uhn.fhir.parser.IParserErrorHandler; 026import ca.uhn.fhir.parser.JsonParser; 027import ca.uhn.fhir.parser.LenientErrorHandler; 028import com.google.gson.Gson; 029import com.google.gson.JsonObject; 030import org.hl7.fhir.instance.model.api.IBaseResource; 031import org.hl7.fhir.instance.model.api.IPrimitiveType; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import javax.annotation.Nullable; 036import java.math.BigDecimal; 037import java.util.Objects; 038 039import static org.apache.commons.lang3.StringUtils.defaultString; 040 041public class TolerantJsonParser extends JsonParser { 042 043 private static final Logger ourLog = LoggerFactory.getLogger(TolerantJsonParser.class); 044 private final FhirContext myContext; 045 private final Long myResourcePid; 046 047 /** 048 * Constructor 049 * 050 * @param theResourcePid The ID of the resource that will be parsed with this parser. It would be ok to change the 051 * datatype for this param if we ever need to since it's only used for logging. 052 */ 053 public TolerantJsonParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler, Long theResourcePid) { 054 super(theContext, theParserErrorHandler); 055 myContext = theContext; 056 myResourcePid = theResourcePid; 057 } 058 059 @Override 060 public <T extends IBaseResource> T parseResource(Class<T> theResourceType, String theMessageString) { 061 try { 062 return super.parseResource(theResourceType, theMessageString); 063 } catch (DataFormatException e) { 064 065 /* 066 * The following is a hacky and gross workaround until the following PR is hopefully merged: 067 * https://github.com/FasterXML/jackson-core/pull/611 068 * 069 * The issue this solves is that under Gson it was possible to store JSON containing 070 * decimal numbers with no leading integer (e.g. .123) and numbers with double leading 071 * zeros (e.g. 000.123). 072 * 073 * These don't parse in Jackson (which is valid behaviour, these aren't ok according to the 074 * JSON spec), meaning we can be stuck with data in the database that can't be loaded back out. 075 * 076 * Note that if we fix this in the future to rely on Jackson natively handing this 077 * nicely we may or may not be able to remove some code from 078 * ParserState.Primitive state too. 079 */ 080 081 String msg = defaultString(e.getMessage(), ""); 082 if (msg.contains("Unexpected character ('.' (code 46))") || msg.contains("Invalid numeric value: Leading zeroes not allowed")) { 083 Gson gson = new Gson(); 084 085 JsonObject object = gson.fromJson(theMessageString, JsonObject.class); 086 String corrected = gson.toJson(object); 087 088 T parsed = super.parseResource(theResourceType, corrected); 089 090 myContext.newTerser().visit(parsed, (theElement, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath) -> { 091 092 BaseRuntimeElementDefinition<?> def = theElementDefinitionPath.get(theElementDefinitionPath.size() - 1); 093 if (def.getName().equals("decimal")) { 094 IPrimitiveType<BigDecimal> decimal = (IPrimitiveType<BigDecimal>) theElement; 095 String oldValue = decimal.getValueAsString(); 096 String newValue = decimal.getValue().toPlainString(); 097 ourLog.warn("Correcting invalid previously saved decimal number for Resource[pid={}] - Was {} and now is {}", 098 Objects.isNull(myResourcePid) ? "" : myResourcePid, oldValue, newValue); 099 decimal.setValueAsString(newValue); 100 } 101 102 return true; 103 }); 104 105 return parsed; 106 } 107 108 throw e; 109 } 110 } 111 112 public static TolerantJsonParser createWithLenientErrorHandling(FhirContext theContext, @Nullable Long theResourcePid) { 113 LenientErrorHandler errorHandler = new LenientErrorHandler(false).disableAllErrors(); 114 return new TolerantJsonParser(theContext, errorHandler, theResourcePid); 115 } 116}