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}