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.json.jackson; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.parser.DataFormatException; 024import ca.uhn.fhir.parser.json.BaseJsonLikeArray; 025import ca.uhn.fhir.parser.json.BaseJsonLikeObject; 026import ca.uhn.fhir.parser.json.BaseJsonLikeValue; 027import ca.uhn.fhir.parser.json.BaseJsonLikeWriter; 028import ca.uhn.fhir.parser.json.JsonLikeStructure; 029import com.fasterxml.jackson.core.JsonGenerator; 030import com.fasterxml.jackson.core.JsonParser; 031import com.fasterxml.jackson.core.JsonProcessingException; 032import com.fasterxml.jackson.core.StreamReadConstraints; 033import com.fasterxml.jackson.core.json.JsonReadFeature; 034import com.fasterxml.jackson.databind.DeserializationFeature; 035import com.fasterxml.jackson.databind.JsonNode; 036import com.fasterxml.jackson.databind.ObjectMapper; 037import com.fasterxml.jackson.databind.json.JsonMapper; 038import com.fasterxml.jackson.databind.node.ArrayNode; 039import com.fasterxml.jackson.databind.node.DecimalNode; 040import com.fasterxml.jackson.databind.node.JsonNodeFactory; 041import com.fasterxml.jackson.databind.node.ObjectNode; 042 043import java.io.IOException; 044import java.io.PushbackReader; 045import java.io.Reader; 046import java.io.Writer; 047import java.math.BigDecimal; 048import java.util.AbstractSet; 049import java.util.ArrayList; 050import java.util.Iterator; 051import java.util.LinkedHashMap; 052import java.util.Map; 053 054public class JacksonStructure implements JsonLikeStructure { 055 056 private static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); 057 private JacksonWriter jacksonWriter; 058 private ROOT_TYPE rootType = null; 059 private JsonNode nativeRoot = null; 060 private JsonNode jsonLikeRoot = null; 061 062 public void setNativeObject(ObjectNode objectNode) { 063 this.rootType = ROOT_TYPE.OBJECT; 064 this.nativeRoot = objectNode; 065 } 066 067 public void setNativeArray(ArrayNode arrayNode) { 068 this.rootType = ROOT_TYPE.ARRAY; 069 this.nativeRoot = arrayNode; 070 } 071 072 @Override 073 public JsonLikeStructure getInstance() { 074 return new JacksonStructure(); 075 } 076 077 @Override 078 public void load(Reader theReader) throws DataFormatException { 079 this.load(theReader, false); 080 } 081 082 @Override 083 public void load(Reader theReader, boolean allowArray) throws DataFormatException { 084 PushbackReader pbr = new PushbackReader(theReader); 085 int nextInt; 086 try { 087 while (true) { 088 nextInt = pbr.read(); 089 if (nextInt == -1) { 090 throw new DataFormatException(Msg.code(1857) + "Did not find any content to parse"); 091 } 092 if (nextInt == '{') { 093 pbr.unread(nextInt); 094 break; 095 } 096 if (Character.isWhitespace(nextInt)) { 097 continue; 098 } 099 if (allowArray) { 100 if (nextInt == '[') { 101 pbr.unread(nextInt); 102 break; 103 } 104 throw new DataFormatException(Msg.code(1858) 105 + "Content does not appear to be FHIR JSON, first non-whitespace character was: '" 106 + (char) nextInt + "' (must be '{' or '[')"); 107 } 108 throw new DataFormatException(Msg.code(1859) 109 + "Content does not appear to be FHIR JSON, first non-whitespace character was: '" 110 + (char) nextInt + "' (must be '{')"); 111 } 112 113 if (nextInt == '{') { 114 setNativeObject((ObjectNode) OBJECT_MAPPER.readTree(pbr)); 115 } else { 116 setNativeArray((ArrayNode) OBJECT_MAPPER.readTree(pbr)); 117 } 118 } catch (Exception e) { 119 String message; 120 if (e instanceof JsonProcessingException) { 121 /* 122 * Currently there is no way of preventing Jackson from adding this 123 * annoying REDACTED message from certain messages we get back from 124 * the parser, so we just manually strip them. Hopefully Jackson 125 * will accept this request at some point: 126 * https://github.com/FasterXML/jackson-core/issues/1158 127 */ 128 JsonProcessingException jpe = (JsonProcessingException) e; 129 StringBuilder messageBuilder = new StringBuilder(); 130 String originalMessage = jpe.getOriginalMessage(); 131 originalMessage = originalMessage.replace( 132 "Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); ", ""); 133 messageBuilder.append(originalMessage); 134 if (jpe.getLocation() != null) { 135 messageBuilder.append("\n at ["); 136 jpe.getLocation().appendOffsetDescription(messageBuilder); 137 messageBuilder.append("]"); 138 } 139 message = messageBuilder.toString(); 140 } else { 141 message = e.getMessage(); 142 } 143 144 if (message.startsWith("Unexpected char 39")) { 145 throw new DataFormatException( 146 Msg.code(1860) + "Failed to parse JSON encoded FHIR content: " + message + " - " 147 + "This may indicate that single quotes are being used as JSON escapes where double quotes are required", 148 e); 149 } 150 throw new DataFormatException(Msg.code(1861) + "Failed to parse JSON encoded FHIR content: " + message, e); 151 } 152 } 153 154 @Override 155 public BaseJsonLikeWriter getJsonLikeWriter(Writer writer) throws IOException { 156 if (null == jacksonWriter) { 157 jacksonWriter = new JacksonWriter(OBJECT_MAPPER.getFactory(), writer); 158 } 159 160 return jacksonWriter; 161 } 162 163 @Override 164 public BaseJsonLikeWriter getJsonLikeWriter() { 165 if (null == jacksonWriter) { 166 jacksonWriter = new JacksonWriter(); 167 } 168 return jacksonWriter; 169 } 170 171 @Override 172 public BaseJsonLikeObject getRootObject() throws DataFormatException { 173 if (rootType == ROOT_TYPE.OBJECT) { 174 if (null == jsonLikeRoot) { 175 jsonLikeRoot = nativeRoot; 176 } 177 178 return new JacksonJsonObject((ObjectNode) jsonLikeRoot); 179 } 180 181 throw new DataFormatException(Msg.code(1862) + "Content must be a valid JSON Object. It must start with '{'."); 182 } 183 184 private enum ROOT_TYPE { 185 OBJECT, 186 ARRAY 187 } 188 189 private static class JacksonJsonObject extends BaseJsonLikeObject { 190 private final ObjectNode nativeObject; 191 192 public JacksonJsonObject(ObjectNode json) { 193 this.nativeObject = json; 194 } 195 196 @Override 197 public Object getValue() { 198 return null; 199 } 200 201 @Override 202 public Iterator<String> keyIterator() { 203 return nativeObject.fieldNames(); 204 } 205 206 @Override 207 public BaseJsonLikeValue get(String key) { 208 JsonNode child = nativeObject.get(key); 209 if (child != null) { 210 return new JacksonJsonValue(child); 211 } 212 return null; 213 } 214 } 215 216 private static class EntryOrderedSet<T> extends AbstractSet<T> { 217 private final transient ArrayList<T> data; 218 219 public EntryOrderedSet() { 220 data = new ArrayList<>(); 221 } 222 223 @Override 224 public int size() { 225 return data.size(); 226 } 227 228 @Override 229 public boolean contains(Object o) { 230 return data.contains(o); 231 } 232 233 public T get(int index) { 234 return data.get(index); 235 } 236 237 @Override 238 public boolean add(T element) { 239 if (data.contains(element)) { 240 return false; 241 } 242 return data.add(element); 243 } 244 245 @Override 246 public boolean remove(Object o) { 247 return data.remove(o); 248 } 249 250 @Override 251 public void clear() { 252 data.clear(); 253 } 254 255 @Override 256 public Iterator<T> iterator() { 257 return data.iterator(); 258 } 259 } 260 261 private static class JacksonJsonArray extends BaseJsonLikeArray { 262 private final ArrayNode nativeArray; 263 private final Map<Integer, BaseJsonLikeValue> jsonLikeMap = new LinkedHashMap<Integer, BaseJsonLikeValue>(); 264 265 public JacksonJsonArray(ArrayNode json) { 266 this.nativeArray = json; 267 } 268 269 @Override 270 public Object getValue() { 271 return null; 272 } 273 274 @Override 275 public int size() { 276 return nativeArray.size(); 277 } 278 279 @Override 280 public BaseJsonLikeValue get(int index) { 281 Integer key = index; 282 BaseJsonLikeValue result = null; 283 if (jsonLikeMap.containsKey(key)) { 284 result = jsonLikeMap.get(key); 285 } else { 286 JsonNode child = nativeArray.get(index); 287 if (child != null) { 288 result = new JacksonJsonValue(child); 289 } 290 jsonLikeMap.put(key, result); 291 } 292 return result; 293 } 294 } 295 296 private static class JacksonJsonValue extends BaseJsonLikeValue { 297 private final JsonNode nativeValue; 298 private BaseJsonLikeObject jsonLikeObject = null; 299 private BaseJsonLikeArray jsonLikeArray = null; 300 301 public JacksonJsonValue(JsonNode jsonNode) { 302 this.nativeValue = jsonNode; 303 } 304 305 @Override 306 public Object getValue() { 307 if (nativeValue != null && nativeValue.isValueNode()) { 308 if (nativeValue.isNumber()) { 309 return nativeValue.numberValue(); 310 } 311 312 if (nativeValue.isBoolean()) { 313 return nativeValue.booleanValue(); 314 } 315 316 return nativeValue.asText(); 317 } 318 return null; 319 } 320 321 @Override 322 public ValueType getJsonType() { 323 if (null == nativeValue) { 324 return ValueType.NULL; 325 } 326 327 switch (nativeValue.getNodeType()) { 328 case NULL: 329 case MISSING: 330 return ValueType.NULL; 331 case OBJECT: 332 return ValueType.OBJECT; 333 case ARRAY: 334 return ValueType.ARRAY; 335 case POJO: 336 case BINARY: 337 case STRING: 338 case NUMBER: 339 case BOOLEAN: 340 default: 341 break; 342 } 343 344 return ValueType.SCALAR; 345 } 346 347 @Override 348 public ScalarType getDataType() { 349 if (nativeValue != null && nativeValue.isValueNode()) { 350 if (nativeValue.isNumber()) { 351 return ScalarType.NUMBER; 352 } 353 if (nativeValue.isTextual()) { 354 return ScalarType.STRING; 355 } 356 if (nativeValue.isBoolean()) { 357 return ScalarType.BOOLEAN; 358 } 359 } 360 return null; 361 } 362 363 @Override 364 public BaseJsonLikeArray getAsArray() { 365 if (nativeValue != null && nativeValue.isArray()) { 366 if (null == jsonLikeArray) { 367 jsonLikeArray = new JacksonJsonArray((ArrayNode) nativeValue); 368 } 369 } 370 return jsonLikeArray; 371 } 372 373 @Override 374 public BaseJsonLikeObject getAsObject() { 375 if (nativeValue != null && nativeValue.isObject()) { 376 if (null == jsonLikeObject) { 377 jsonLikeObject = new JacksonJsonObject((ObjectNode) nativeValue); 378 } 379 } 380 return jsonLikeObject; 381 } 382 383 @Override 384 public Number getAsNumber() { 385 return nativeValue != null ? nativeValue.numberValue() : null; 386 } 387 388 @Override 389 public String getAsString() { 390 if (nativeValue != null) { 391 if (nativeValue instanceof DecimalNode) { 392 BigDecimal value = nativeValue.decimalValue(); 393 return value.toPlainString(); 394 } 395 return nativeValue.asText(); 396 } 397 return null; 398 } 399 400 @Override 401 public boolean getAsBoolean() { 402 if (nativeValue != null && nativeValue.isValueNode() && nativeValue.isBoolean()) { 403 return nativeValue.asBoolean(); 404 } 405 return super.getAsBoolean(); 406 } 407 } 408 409 private static ObjectMapper createObjectMapper() { 410 ObjectMapper retVal = JsonMapper.builder() 411 .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS) 412 .build(); 413 retVal = retVal.setNodeFactory(new JsonNodeFactory(true)); 414 retVal = retVal.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); 415 retVal = retVal.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); 416 retVal = retVal.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); 417 retVal = retVal.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); 418 retVal = retVal.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); 419 retVal = retVal.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); 420 421 retVal.getFactory().setStreamReadConstraints(createStreamReadConstraints()); 422 423 return retVal; 424 } 425 426 private static StreamReadConstraints createStreamReadConstraints() { 427 return StreamReadConstraints.builder() 428 .maxStringLength(Integer.MAX_VALUE) 429 .build(); 430 } 431}