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}