001package ca.uhn.fhir.parser.json.jackson; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.parser.DataFormatException; 024import ca.uhn.fhir.parser.json.JsonLikeArray; 025import ca.uhn.fhir.parser.json.JsonLikeObject; 026import ca.uhn.fhir.parser.json.JsonLikeStructure; 027import ca.uhn.fhir.parser.json.JsonLikeValue; 028import ca.uhn.fhir.parser.json.JsonLikeWriter; 029import com.fasterxml.jackson.core.JsonGenerator; 030import com.fasterxml.jackson.core.JsonParser; 031import com.fasterxml.jackson.databind.DeserializationFeature; 032import com.fasterxml.jackson.databind.JsonNode; 033import com.fasterxml.jackson.databind.ObjectMapper; 034import com.fasterxml.jackson.databind.node.ArrayNode; 035import com.fasterxml.jackson.databind.node.DecimalNode; 036import com.fasterxml.jackson.databind.node.JsonNodeFactory; 037import com.fasterxml.jackson.databind.node.ObjectNode; 038 039import java.io.IOException; 040import java.io.PushbackReader; 041import java.io.Reader; 042import java.io.Writer; 043import java.math.BigDecimal; 044import java.util.AbstractSet; 045import java.util.ArrayList; 046import java.util.Iterator; 047import java.util.LinkedHashMap; 048import java.util.Map; 049import java.util.Set; 050import java.util.stream.Collectors; 051import java.util.stream.StreamSupport; 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("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("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char) nextInt + "' (must be '{' or '[')"); 104 } 105 throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char) nextInt + "' (must be '{')"); 106 } 107 108 if (nextInt == '{') { 109 setNativeObject((ObjectNode) OBJECT_MAPPER.readTree(pbr)); 110 } else { 111 setNativeArray((ArrayNode) OBJECT_MAPPER.readTree(pbr)); 112 } 113 } catch (Exception e) { 114 if (e.getMessage().startsWith("Unexpected char 39")) { 115 throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage() + " - " + 116 "This may indicate that single quotes are being used as JSON escapes where double quotes are required", e); 117 } 118 throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage(), e); 119 } 120 } 121 122 @Override 123 public JsonLikeWriter getJsonLikeWriter(Writer writer) throws IOException { 124 if (null == jacksonWriter) { 125 jacksonWriter = new JacksonWriter(OBJECT_MAPPER.getFactory(), writer); 126 } 127 128 return jacksonWriter; 129 } 130 131 @Override 132 public JsonLikeWriter getJsonLikeWriter() { 133 if (null == jacksonWriter) { 134 jacksonWriter = new JacksonWriter(); 135 } 136 return jacksonWriter; 137 } 138 139 @Override 140 public JsonLikeObject getRootObject() throws DataFormatException { 141 if (rootType == ROOT_TYPE.OBJECT) { 142 if (null == jsonLikeRoot) { 143 jsonLikeRoot = nativeRoot; 144 } 145 146 return new JacksonJsonObject((ObjectNode) jsonLikeRoot); 147 } 148 149 throw new DataFormatException("Content must be a valid JSON Object. It must start with '{'."); 150 } 151 152 private enum ROOT_TYPE {OBJECT, ARRAY} 153 154 private static class JacksonJsonObject extends JsonLikeObject { 155 private final ObjectNode nativeObject; 156 private final Map<String, JsonLikeValue> jsonLikeMap = new LinkedHashMap<>(); 157 private Set<String> keySet = null; 158 159 public JacksonJsonObject(ObjectNode json) { 160 this.nativeObject = json; 161 } 162 163 @Override 164 public Object getValue() { 165 return null; 166 } 167 168 @Override 169 public Set<String> keySet() { 170 if (null == keySet) { 171 final Iterable<Map.Entry<String, JsonNode>> iterable = nativeObject::fields; 172 keySet = StreamSupport.stream(iterable.spliterator(), false) 173 .map(Map.Entry::getKey) 174 .collect(Collectors.toCollection(EntryOrderedSet::new)); 175 } 176 177 return keySet; 178 } 179 180 @Override 181 public JsonLikeValue get(String key) { 182 JsonLikeValue result = null; 183 if (jsonLikeMap.containsKey(key)) { 184 result = jsonLikeMap.get(key); 185 } else { 186 JsonNode child = nativeObject.get(key); 187 if (child != null) { 188 result = new JacksonJsonValue(child); 189 } 190 jsonLikeMap.put(key, result); 191 } 192 return result; 193 } 194 } 195 196 private static class EntryOrderedSet<T> extends AbstractSet<T> { 197 private final transient ArrayList<T> data; 198 199 public EntryOrderedSet() { 200 data = new ArrayList<>(); 201 } 202 203 @Override 204 public int size() { 205 return data.size(); 206 } 207 208 @Override 209 public boolean contains(Object o) { 210 return data.contains(o); 211 } 212 213 public T get(int index) { 214 return data.get(index); 215 } 216 217 @Override 218 public boolean add(T element) { 219 if (data.contains(element)) { 220 return false; 221 } 222 return data.add(element); 223 } 224 225 @Override 226 public boolean remove(Object o) { 227 return data.remove(o); 228 } 229 230 @Override 231 public void clear() { 232 data.clear(); 233 } 234 235 @Override 236 public Iterator<T> iterator() { 237 return data.iterator(); 238 } 239 } 240 241 private static class JacksonJsonArray extends JsonLikeArray { 242 private final ArrayNode nativeArray; 243 private final Map<Integer, JsonLikeValue> jsonLikeMap = new LinkedHashMap<Integer, JsonLikeValue>(); 244 245 public JacksonJsonArray(ArrayNode json) { 246 this.nativeArray = json; 247 } 248 249 @Override 250 public Object getValue() { 251 return null; 252 } 253 254 @Override 255 public int size() { 256 return nativeArray.size(); 257 } 258 259 @Override 260 public JsonLikeValue get(int index) { 261 Integer key = index; 262 JsonLikeValue result = null; 263 if (jsonLikeMap.containsKey(key)) { 264 result = jsonLikeMap.get(key); 265 } else { 266 JsonNode child = nativeArray.get(index); 267 if (child != null) { 268 result = new JacksonJsonValue(child); 269 } 270 jsonLikeMap.put(key, result); 271 } 272 return result; 273 } 274 } 275 276 private static class JacksonJsonValue extends JsonLikeValue { 277 private final JsonNode nativeValue; 278 private JsonLikeObject jsonLikeObject = null; 279 private JsonLikeArray jsonLikeArray = null; 280 281 public JacksonJsonValue(JsonNode jsonNode) { 282 this.nativeValue = jsonNode; 283 } 284 285 @Override 286 public Object getValue() { 287 if (nativeValue != null && nativeValue.isValueNode()) { 288 if (nativeValue.isNumber()) { 289 return nativeValue.numberValue(); 290 } 291 292 if (nativeValue.isBoolean()) { 293 return nativeValue.booleanValue(); 294 } 295 296 return nativeValue.asText(); 297 } 298 return null; 299 } 300 301 @Override 302 public ValueType getJsonType() { 303 if (null == nativeValue || nativeValue.isNull()) { 304 return ValueType.NULL; 305 } 306 if (nativeValue.isObject()) { 307 return ValueType.OBJECT; 308 } 309 if (nativeValue.isArray()) { 310 return ValueType.ARRAY; 311 } 312 if (nativeValue.isValueNode()) { 313 return ValueType.SCALAR; 314 } 315 return null; 316 } 317 318 @Override 319 public ScalarType getDataType() { 320 if (nativeValue != null && nativeValue.isValueNode()) { 321 if (nativeValue.isNumber()) { 322 return ScalarType.NUMBER; 323 } 324 if (nativeValue.isTextual()) { 325 return ScalarType.STRING; 326 } 327 if (nativeValue.isBoolean()) { 328 return ScalarType.BOOLEAN; 329 } 330 } 331 return null; 332 } 333 334 @Override 335 public JsonLikeArray getAsArray() { 336 if (nativeValue != null && nativeValue.isArray()) { 337 if (null == jsonLikeArray) { 338 jsonLikeArray = new JacksonJsonArray((ArrayNode) nativeValue); 339 } 340 } 341 return jsonLikeArray; 342 } 343 344 @Override 345 public JsonLikeObject getAsObject() { 346 if (nativeValue != null && nativeValue.isObject()) { 347 if (null == jsonLikeObject) { 348 jsonLikeObject = new JacksonJsonObject((ObjectNode) nativeValue); 349 } 350 } 351 return jsonLikeObject; 352 } 353 354 @Override 355 public Number getAsNumber() { 356 return nativeValue != null ? nativeValue.numberValue() : null; 357 } 358 359 @Override 360 public String getAsString() { 361 if (nativeValue != null) { 362 if (nativeValue instanceof DecimalNode) { 363 BigDecimal value = nativeValue.decimalValue(); 364 return value.toPlainString(); 365 } 366 return nativeValue.asText(); 367 } 368 return null; 369 } 370 371 @Override 372 public boolean getAsBoolean() { 373 if (nativeValue != null && nativeValue.isValueNode() && nativeValue.isBoolean()) { 374 return nativeValue.asBoolean(); 375 } 376 return super.getAsBoolean(); 377 } 378 } 379 380 private static ObjectMapper createObjectMapper() { 381 ObjectMapper retVal = new ObjectMapper(); 382 retVal = retVal.setNodeFactory(new JsonNodeFactory(true)); 383 retVal = retVal.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); 384 retVal = retVal.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); 385 retVal = retVal.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); 386 retVal = retVal.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); 387 retVal = retVal.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); 388 retVal = retVal.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); 389 return retVal; 390 } 391}