001/*- 002 * #%L 003 * HAPI FHIR JPA Server 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.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 jakarta.annotation.Nullable; 031import org.hl7.fhir.instance.model.api.IBaseResource; 032import org.hl7.fhir.instance.model.api.IPrimitiveType; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 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))") 083 || msg.contains("Invalid numeric value: Leading zeroes not allowed")) { 084 Gson gson = new Gson(); 085 086 JsonObject object = gson.fromJson(theMessageString, JsonObject.class); 087 String corrected = gson.toJson(object); 088 089 T parsed = super.parseResource(theResourceType, corrected); 090 091 myContext 092 .newTerser() 093 .visit( 094 parsed, 095 (theElement, 096 theContainingElementPath, 097 theChildDefinitionPath, 098 theElementDefinitionPath) -> { 099 BaseRuntimeElementDefinition<?> def = 100 theElementDefinitionPath.get(theElementDefinitionPath.size() - 1); 101 if (def.getName().equals("decimal")) { 102 IPrimitiveType<BigDecimal> decimal = (IPrimitiveType<BigDecimal>) theElement; 103 String oldValue = decimal.getValueAsString(); 104 String newValue = decimal.getValue().toPlainString(); 105 ourLog.warn( 106 "Correcting invalid previously saved decimal number for Resource[pid={}] - Was {} and now is {}", 107 Objects.isNull(myResourcePid) ? "" : myResourcePid, 108 oldValue, 109 newValue); 110 decimal.setValueAsString(newValue); 111 } 112 113 return true; 114 }); 115 116 return parsed; 117 } 118 119 throw e; 120 } 121 } 122 123 public static TolerantJsonParser createWithLenientErrorHandling( 124 FhirContext theContext, @Nullable Long theResourcePid) { 125 LenientErrorHandler errorHandler = new LenientErrorHandler(false).disableAllErrors(); 126 return new TolerantJsonParser(theContext, errorHandler, theResourcePid); 127 } 128}