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}