001package ca.uhn.fhir.parser.json.jackson;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2021 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.parser.DataFormatException;
024import ca.uhn.fhir.parser.json.JsonLikeArray;
025import ca.uhn.fhir.parser.json.JsonLikeObject;
026import ca.uhn.fhir.parser.json.JsonLikeStructure;
027import ca.uhn.fhir.parser.json.JsonLikeValue;
028import ca.uhn.fhir.parser.json.JsonLikeWriter;
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.node.ArrayNode;
035import com.fasterxml.jackson.databind.node.DecimalNode;
036import com.fasterxml.jackson.databind.node.JsonNodeFactory;
037import com.fasterxml.jackson.databind.node.ObjectNode;
038
039import java.io.IOException;
040import java.io.PushbackReader;
041import java.io.Reader;
042import java.io.Writer;
043import java.math.BigDecimal;
044import java.util.AbstractSet;
045import java.util.ArrayList;
046import java.util.Iterator;
047import java.util.LinkedHashMap;
048import java.util.Map;
049import java.util.Set;
050import java.util.stream.Collectors;
051import java.util.stream.StreamSupport;
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("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("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char) nextInt + "' (must be '{' or '[')");
104                                }
105                                throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char) nextInt + "' (must be '{')");
106                        }
107
108                        if (nextInt == '{') {
109                                setNativeObject((ObjectNode) OBJECT_MAPPER.readTree(pbr));
110                        } else {
111                                setNativeArray((ArrayNode) OBJECT_MAPPER.readTree(pbr));
112                        }
113                } catch (Exception e) {
114                        if (e.getMessage().startsWith("Unexpected char 39")) {
115                                throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage() + " - " +
116                                        "This may indicate that single quotes are being used as JSON escapes where double quotes are required", e);
117                        }
118                        throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage(), e);
119                }
120        }
121
122        @Override
123        public JsonLikeWriter getJsonLikeWriter(Writer writer) throws IOException {
124                if (null == jacksonWriter) {
125                        jacksonWriter = new JacksonWriter(OBJECT_MAPPER.getFactory(), writer);
126                }
127
128                return jacksonWriter;
129        }
130
131        @Override
132        public JsonLikeWriter getJsonLikeWriter() {
133                if (null == jacksonWriter) {
134                        jacksonWriter = new JacksonWriter();
135                }
136                return jacksonWriter;
137        }
138
139        @Override
140        public JsonLikeObject getRootObject() throws DataFormatException {
141                if (rootType == ROOT_TYPE.OBJECT) {
142                        if (null == jsonLikeRoot) {
143                                jsonLikeRoot = nativeRoot;
144                        }
145
146                        return new JacksonJsonObject((ObjectNode) jsonLikeRoot);
147                }
148
149                throw new DataFormatException("Content must be a valid JSON Object. It must start with '{'.");
150        }
151
152        private enum ROOT_TYPE {OBJECT, ARRAY}
153
154        private static class JacksonJsonObject extends JsonLikeObject {
155                private final ObjectNode nativeObject;
156                private final Map<String, JsonLikeValue> jsonLikeMap = new LinkedHashMap<>();
157                private Set<String> keySet = null;
158
159                public JacksonJsonObject(ObjectNode json) {
160                        this.nativeObject = json;
161                }
162
163                @Override
164                public Object getValue() {
165                        return null;
166                }
167
168                @Override
169                public Set<String> keySet() {
170                        if (null == keySet) {
171                                final Iterable<Map.Entry<String, JsonNode>> iterable = nativeObject::fields;
172                                keySet = StreamSupport.stream(iterable.spliterator(), false)
173                                        .map(Map.Entry::getKey)
174                                        .collect(Collectors.toCollection(EntryOrderedSet::new));
175                        }
176
177                        return keySet;
178                }
179
180                @Override
181                public JsonLikeValue get(String key) {
182                        JsonLikeValue result = null;
183                        if (jsonLikeMap.containsKey(key)) {
184                                result = jsonLikeMap.get(key);
185                        } else {
186                                JsonNode child = nativeObject.get(key);
187                                if (child != null) {
188                                        result = new JacksonJsonValue(child);
189                                }
190                                jsonLikeMap.put(key, result);
191                        }
192                        return result;
193                }
194        }
195
196        private static class EntryOrderedSet<T> extends AbstractSet<T> {
197                private final transient ArrayList<T> data;
198
199                public EntryOrderedSet() {
200                        data = new ArrayList<>();
201                }
202
203                @Override
204                public int size() {
205                        return data.size();
206                }
207
208                @Override
209                public boolean contains(Object o) {
210                        return data.contains(o);
211                }
212
213                public T get(int index) {
214                        return data.get(index);
215                }
216
217                @Override
218                public boolean add(T element) {
219                        if (data.contains(element)) {
220                                return false;
221                        }
222                        return data.add(element);
223                }
224
225                @Override
226                public boolean remove(Object o) {
227                        return data.remove(o);
228                }
229
230                @Override
231                public void clear() {
232                        data.clear();
233                }
234
235                @Override
236                public Iterator<T> iterator() {
237                        return data.iterator();
238                }
239        }
240
241        private static class JacksonJsonArray extends JsonLikeArray {
242                private final ArrayNode nativeArray;
243                private final Map<Integer, JsonLikeValue> jsonLikeMap = new LinkedHashMap<Integer, JsonLikeValue>();
244
245                public JacksonJsonArray(ArrayNode json) {
246                        this.nativeArray = json;
247                }
248
249                @Override
250                public Object getValue() {
251                        return null;
252                }
253
254                @Override
255                public int size() {
256                        return nativeArray.size();
257                }
258
259                @Override
260                public JsonLikeValue get(int index) {
261                        Integer key = index;
262                        JsonLikeValue result = null;
263                        if (jsonLikeMap.containsKey(key)) {
264                                result = jsonLikeMap.get(key);
265                        } else {
266                                JsonNode child = nativeArray.get(index);
267                                if (child != null) {
268                                        result = new JacksonJsonValue(child);
269                                }
270                                jsonLikeMap.put(key, result);
271                        }
272                        return result;
273                }
274        }
275
276        private static class JacksonJsonValue extends JsonLikeValue {
277                private final JsonNode nativeValue;
278                private JsonLikeObject jsonLikeObject = null;
279                private JsonLikeArray jsonLikeArray = null;
280
281                public JacksonJsonValue(JsonNode jsonNode) {
282                        this.nativeValue = jsonNode;
283                }
284
285                @Override
286                public Object getValue() {
287                        if (nativeValue != null && nativeValue.isValueNode()) {
288                                if (nativeValue.isNumber()) {
289                                        return nativeValue.numberValue();
290                                }
291
292                                if (nativeValue.isBoolean()) {
293                                        return nativeValue.booleanValue();
294                                }
295
296                                return nativeValue.asText();
297                        }
298                        return null;
299                }
300
301                @Override
302                public ValueType getJsonType() {
303                        if (null == nativeValue || nativeValue.isNull()) {
304                                return ValueType.NULL;
305                        }
306                        if (nativeValue.isObject()) {
307                                return ValueType.OBJECT;
308                        }
309                        if (nativeValue.isArray()) {
310                                return ValueType.ARRAY;
311                        }
312                        if (nativeValue.isValueNode()) {
313                                return ValueType.SCALAR;
314                        }
315                        return null;
316                }
317
318                @Override
319                public ScalarType getDataType() {
320                        if (nativeValue != null && nativeValue.isValueNode()) {
321                                if (nativeValue.isNumber()) {
322                                        return ScalarType.NUMBER;
323                                }
324                                if (nativeValue.isTextual()) {
325                                        return ScalarType.STRING;
326                                }
327                                if (nativeValue.isBoolean()) {
328                                        return ScalarType.BOOLEAN;
329                                }
330                        }
331                        return null;
332                }
333
334                @Override
335                public JsonLikeArray getAsArray() {
336                        if (nativeValue != null && nativeValue.isArray()) {
337                                if (null == jsonLikeArray) {
338                                        jsonLikeArray = new JacksonJsonArray((ArrayNode) nativeValue);
339                                }
340                        }
341                        return jsonLikeArray;
342                }
343
344                @Override
345                public JsonLikeObject getAsObject() {
346                        if (nativeValue != null && nativeValue.isObject()) {
347                                if (null == jsonLikeObject) {
348                                        jsonLikeObject = new JacksonJsonObject((ObjectNode) nativeValue);
349                                }
350                        }
351                        return jsonLikeObject;
352                }
353
354                @Override
355                public Number getAsNumber() {
356                        return nativeValue != null ? nativeValue.numberValue() : null;
357                }
358
359                @Override
360                public String getAsString() {
361                        if (nativeValue != null) {
362                                if (nativeValue instanceof DecimalNode) {
363                                        BigDecimal value = nativeValue.decimalValue();
364                                        return value.toPlainString();
365                                }
366                                return nativeValue.asText();
367                        }
368                        return null;
369                }
370
371                @Override
372                public boolean getAsBoolean() {
373                        if (nativeValue != null && nativeValue.isValueNode() && nativeValue.isBoolean()) {
374                                return nativeValue.asBoolean();
375                        }
376                        return super.getAsBoolean();
377                }
378        }
379
380        private static ObjectMapper createObjectMapper() {
381                ObjectMapper retVal = new ObjectMapper();
382                retVal = retVal.setNodeFactory(new JsonNodeFactory(true));
383                retVal = retVal.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
384                retVal = retVal.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
385                retVal = retVal.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION);
386                retVal = retVal.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
387                retVal = retVal.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
388                retVal = retVal.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
389                return retVal;
390        }
391}