001package ca.uhn.fhir.parser.json;
002/*
003 * #%L
004 * HAPI FHIR - Core Library
005 * %%
006 * Copyright (C) 2014 - 2020 University Health Network
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *      http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * #L%
020 */
021
022import java.io.PushbackReader;
023import java.io.Reader;
024import java.io.Writer;
025import java.util.AbstractSet;
026import java.util.ArrayList;
027import java.util.Iterator;
028import java.util.LinkedHashMap;
029import java.util.Map;
030import java.util.Map.Entry;
031import java.util.Set;
032
033import ca.uhn.fhir.parser.DataFormatException;
034
035import com.google.gson.Gson;
036import com.google.gson.GsonBuilder;
037import com.google.gson.JsonArray;
038import com.google.gson.JsonElement;
039import com.google.gson.JsonObject;
040import com.google.gson.JsonPrimitive;
041import com.google.gson.JsonSyntaxException;
042
043public class GsonStructure implements JsonLikeStructure {
044
045        private enum ROOT_TYPE {OBJECT, ARRAY};
046        private ROOT_TYPE rootType = null;
047        private JsonElement nativeRoot = null;
048        private JsonLikeValue jsonLikeRoot = null;
049        private GsonWriter jsonLikeWriter = null;
050        
051        public GsonStructure() {
052                super();
053        }
054        
055        public void setNativeObject (JsonObject json) {
056                this.rootType = ROOT_TYPE.OBJECT;
057                this.nativeRoot = json;
058        }
059        public void setNativeArray (JsonArray json) {
060                this.rootType = ROOT_TYPE.ARRAY;
061                this.nativeRoot = json;
062        }
063
064        @Override
065        public JsonLikeStructure getInstance() {
066                return new GsonStructure();
067        }
068
069        @Override
070        public void load(Reader theReader) throws DataFormatException {
071                this.load(theReader, false);            
072        }
073
074        @Override
075        public void load(Reader theReader, boolean allowArray) throws DataFormatException {
076                PushbackReader pbr = new PushbackReader(theReader);
077                int nextInt;
078                try {
079                        while(true) {
080                                        nextInt = pbr.read();
081                                if (nextInt == -1) {
082                                        throw new DataFormatException("Did not find any content to parse");
083                                }
084                                if (nextInt == '{') {
085                                        pbr.unread(nextInt);
086                                        break;
087                                }
088                                if (Character.isWhitespace(nextInt)) {
089                                        continue;
090                                }
091                                if (allowArray) {
092                                        if (nextInt == '[') {
093                                                pbr.unread(nextInt);
094                                                break;
095                                        }
096                                        throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{' or '[')");
097                                }
098                                throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{')");
099                        }
100                
101                        Gson gson = new GsonBuilder().disableHtmlEscaping().create();
102                        if (nextInt == '{') {
103                                JsonObject root = gson.fromJson(pbr, JsonObject.class);
104                                setNativeObject(root);
105                        } else if (nextInt == '[') {
106                                JsonArray root = gson.fromJson(pbr, JsonArray.class);
107                                setNativeArray(root);
108                        }
109                } catch (JsonSyntaxException e) {
110                        if (e.getMessage().startsWith("Unexpected char 39")) {
111                                throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage() + " - This may indicate that single quotes are being used as JSON escapes where double quotes are required", e);
112                        }
113                        throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage(), e);
114                } catch (Exception e) {
115                        throw new DataFormatException("Failed to parse JSON content, error was: " + e.getMessage(), e);
116                }
117        }
118
119        @Override
120        public JsonLikeWriter getJsonLikeWriter (Writer writer) {
121                if (null == jsonLikeWriter) {
122                        jsonLikeWriter = new GsonWriter(writer);
123                }
124                return jsonLikeWriter;
125        }
126
127        @Override
128        public JsonLikeWriter getJsonLikeWriter () {
129                if (null == jsonLikeWriter) {
130                        jsonLikeWriter = new GsonWriter();
131                }
132                return jsonLikeWriter;
133        }
134
135        @Override
136        public JsonLikeObject getRootObject() throws DataFormatException {
137                if (rootType == ROOT_TYPE.OBJECT) {
138                        if (null == jsonLikeRoot) {
139                                jsonLikeRoot = new GsonJsonObject((JsonObject)nativeRoot);
140                        }
141                        return jsonLikeRoot.getAsObject();
142                }
143                throw new DataFormatException("Content must be a valid JSON Object. It must start with '{'.");
144        }
145
146        @Override
147        public JsonLikeArray getRootArray() throws DataFormatException {
148                if (rootType == ROOT_TYPE.ARRAY) {
149                        if (null == jsonLikeRoot) {
150                                jsonLikeRoot = new GsonJsonArray((JsonArray)nativeRoot);
151                        }
152                        return jsonLikeRoot.getAsArray();
153                }
154                throw new DataFormatException("Content must be a valid JSON Array. It must start with '['.");
155        }
156
157        private static class GsonJsonObject extends JsonLikeObject {
158                private JsonObject nativeObject;
159                private Set<String> keySet = null;
160                private Map<String,JsonLikeValue> jsonLikeMap = new LinkedHashMap<String,JsonLikeValue>();
161                
162                public GsonJsonObject (JsonObject json) {
163                        this.nativeObject = json;
164                }
165
166                @Override
167                public Object getValue() {
168                        return null;
169                }
170
171                @Override
172                public Set<String> keySet() {
173                        if (null == keySet) {
174                                Set<Entry<String, JsonElement>> entrySet = nativeObject.entrySet();
175                                keySet = new EntryOrderedSet<String>(entrySet.size());
176                                for (Entry<String,?> entry : entrySet) {
177                                        keySet.add(entry.getKey());
178                                }
179                        }
180                        return keySet;
181                }
182
183                @Override
184                public JsonLikeValue get(String key) {
185                        JsonLikeValue result = null;
186                        if (jsonLikeMap.containsKey(key)) {
187                                result = jsonLikeMap.get(key); 
188                        } else {
189                                JsonElement child = nativeObject.get(key);
190                                if (child != null) {
191                                        result = new GsonJsonValue(child);
192                                }
193                                jsonLikeMap.put(key, result);
194                        }
195                        return result;
196                }
197        }
198        
199        private static class GsonJsonArray extends JsonLikeArray {
200                private JsonArray nativeArray;
201                private Map<Integer,JsonLikeValue> jsonLikeMap = new LinkedHashMap<Integer,JsonLikeValue>();
202                
203                public GsonJsonArray (JsonArray json) {
204                        this.nativeArray = json;
205                }
206
207                @Override
208                public Object getValue() {
209                        return null;
210                }
211
212                @Override
213                public int size() {
214                        return nativeArray.size();
215                }
216
217                @Override
218                public JsonLikeValue get(int index) {
219                        Integer key = Integer.valueOf(index);
220                        JsonLikeValue result = null;
221                        if (jsonLikeMap.containsKey(key)) {
222                                result = jsonLikeMap.get(key); 
223                        } else {
224                                JsonElement child = nativeArray.get(index);
225                                if (child != null) {
226                                        result = new GsonJsonValue(child);
227                                }
228                                jsonLikeMap.put(key, result);
229                        }
230                        return result;
231                }
232        }
233        
234        private static class GsonJsonValue extends JsonLikeValue {
235                private JsonElement nativeValue;
236                private JsonLikeObject jsonLikeObject = null;
237                private JsonLikeArray jsonLikeArray = null;
238                
239                public GsonJsonValue (JsonElement json) {
240                        this.nativeValue = json;
241                }
242
243                @Override
244                public Object getValue() {
245                        if (nativeValue != null && nativeValue.isJsonPrimitive()) {
246                                if (((JsonPrimitive)nativeValue).isNumber()) {
247                                        return nativeValue.getAsNumber();
248                                }
249                                if (((JsonPrimitive)nativeValue).isBoolean()) {
250                                        return Boolean.valueOf(nativeValue.getAsBoolean());
251                                }
252                                return nativeValue.getAsString();
253                        }
254                        return null;
255                }
256                
257                @Override
258                public ValueType getJsonType() {
259                        if (null == nativeValue || nativeValue.isJsonNull()) {
260                                return ValueType.NULL;
261                        }
262                        if (nativeValue.isJsonObject()) {
263                                return ValueType.OBJECT;
264                        }
265                        if (nativeValue.isJsonArray()) {
266                                return ValueType.ARRAY;
267                        }
268                        if (nativeValue.isJsonPrimitive()) {
269                                return ValueType.SCALAR;
270                        }
271                        return null;
272                }
273                
274                @Override
275                public ScalarType getDataType() {
276                        if (nativeValue != null && nativeValue.isJsonPrimitive()) {
277                                if (((JsonPrimitive)nativeValue).isNumber()) {
278                                        return ScalarType.NUMBER;
279                                }
280                                if (((JsonPrimitive)nativeValue).isString()) {
281                                        return ScalarType.STRING;
282                                }
283                                if (((JsonPrimitive)nativeValue).isBoolean()) {
284                                        return ScalarType.BOOLEAN;
285                                }
286                        }
287                        return null;
288                }
289
290                @Override
291                public JsonLikeArray getAsArray() {
292                        if (nativeValue != null && nativeValue.isJsonArray()) {
293                                if (null == jsonLikeArray) {
294                                        jsonLikeArray = new GsonJsonArray((JsonArray)nativeValue);
295                                }
296                        }
297                        return jsonLikeArray;
298                }
299
300                @Override
301                public JsonLikeObject getAsObject() {
302                        if (nativeValue != null && nativeValue.isJsonObject()) {
303                                if (null == jsonLikeObject) {
304                                        jsonLikeObject = new GsonJsonObject((JsonObject)nativeValue);
305                                }
306                        }
307                        return jsonLikeObject;
308                }
309
310                @Override
311                public Number getAsNumber() {
312                        return nativeValue != null ? nativeValue.getAsNumber() : null;
313                }
314
315                @Override
316                public String getAsString() {
317                        return nativeValue != null ? nativeValue.getAsString() : null;
318                }
319
320                @Override
321                public boolean getAsBoolean() {
322                        if (nativeValue != null && nativeValue.isJsonPrimitive() && ((JsonPrimitive)nativeValue).isBoolean()) {
323                                return nativeValue.getAsBoolean();
324                        }
325                        return super.getAsBoolean();
326                }
327        }
328        
329        private static class EntryOrderedSet<T> extends AbstractSet<T> {
330                private transient ArrayList<T> data = null;
331                
332                public EntryOrderedSet (int initialCapacity) {
333                        data = new ArrayList<T>(initialCapacity);
334                }
335                @SuppressWarnings("unused")
336                public EntryOrderedSet () {
337                        data = new ArrayList<T>();
338                }
339                
340                @Override
341                public int size() {
342                        return data.size();
343                }
344
345                @Override
346                public boolean contains(Object o) {
347                        return data.contains(o);
348                }
349
350                @SuppressWarnings("unused")  // not really.. just not here
351                public T get(int index) {
352                        return data.get(index);
353                }
354                
355                @Override
356                public boolean add(T element) {
357                        if (data.contains(element)) {
358                                return false;
359                        }
360                        return data.add(element);
361                }
362                
363                @Override
364                public boolean remove(Object o) {
365                        return data.remove(o);
366                }
367
368                @Override
369                public void clear() {
370                        data.clear();
371                }
372                
373                @Override
374                public Iterator<T> iterator() {
375                        return data.iterator();
376                }
377                
378        }
379}