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