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