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