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