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}