001package ca.uhn.fhir.parser.json;
002/*
003 * #%L
004 * HAPI FHIR - Core Library
005 * %%
006 * Copyright (C) 2014 - 2019 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 GsonStructure (JsonObject json) {
056                super();
057                setNativeObject(json);
058        }
059        public GsonStructure (JsonArray json) {
060                super();
061                setNativeArray(json);
062        }
063        
064        public void setNativeObject (JsonObject json) {
065                this.rootType = ROOT_TYPE.OBJECT;
066                this.nativeRoot = json;
067        }
068        public void setNativeArray (JsonArray json) {
069                this.rootType = ROOT_TYPE.ARRAY;
070                this.nativeRoot = json;
071        }
072
073        @Override
074        public JsonLikeStructure getInstance() {
075                return new GsonStructure();
076        }
077
078        @Override
079        public void load(Reader theReader) throws DataFormatException {
080                this.load(theReader, false);            
081        }
082
083        @Override
084        public void load(Reader theReader, boolean allowArray) throws DataFormatException {
085                PushbackReader pbr = new PushbackReader(theReader);
086                int nextInt;
087                try {
088                        while(true) {
089                                        nextInt = pbr.read();
090                                if (nextInt == -1) {
091                                        throw new DataFormatException("Did not find any content to parse");
092                                }
093                                if (nextInt == '{') {
094                                        pbr.unread(nextInt);
095                                        break;
096                                }
097                                if (Character.isWhitespace(nextInt)) {
098                                        continue;
099                                }
100                                if (allowArray) {
101                                        if (nextInt == '[') {
102                                                pbr.unread(nextInt);
103                                                break;
104                                        }
105                                        throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{' or '[')");
106                                }
107                                throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{')");
108                        }
109                
110                        Gson gson = new GsonBuilder().disableHtmlEscaping().create();
111                        if (nextInt == '{') {
112                                JsonObject root = gson.fromJson(pbr, JsonObject.class);
113                                setNativeObject(root);
114                        } else
115                        if (nextInt == '[') {
116                                JsonArray root = gson.fromJson(pbr, JsonArray.class);
117                                setNativeArray(root);
118                        }
119                } catch (JsonSyntaxException e) {
120                        if (e.getMessage().startsWith("Unexpected char 39")) {
121                                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);
122                        }
123                        throw new DataFormatException("Failed to parse JSON encoded FHIR content: " + e.getMessage(), e);
124                } catch (Exception e) {
125                        throw new DataFormatException("Failed to parse JSON content, error was: " + e.getMessage(), e);
126                }
127        }
128
129        @Override
130        public JsonLikeWriter getJsonLikeWriter (Writer writer) {
131                if (null == jsonLikeWriter) {
132                        jsonLikeWriter = new GsonWriter(writer);
133                }
134                return jsonLikeWriter;
135        }
136
137        @Override
138        public JsonLikeWriter getJsonLikeWriter () {
139                if (null == jsonLikeWriter) {
140                        jsonLikeWriter = new GsonWriter();
141                }
142                return jsonLikeWriter;
143        }
144
145        @Override
146        public JsonLikeObject getRootObject() throws DataFormatException {
147                if (rootType == ROOT_TYPE.OBJECT) {
148                        if (null == jsonLikeRoot) {
149                                jsonLikeRoot = new GsonJsonObject((JsonObject)nativeRoot);
150                        }
151                        return jsonLikeRoot.getAsObject();
152                }
153                throw new DataFormatException("Content must be a valid JSON Object. It must start with '{'.");
154        }
155
156        @Override
157        public JsonLikeArray getRootArray() throws DataFormatException {
158                if (rootType == ROOT_TYPE.ARRAY) {
159                        if (null == jsonLikeRoot) {
160                                jsonLikeRoot = new GsonJsonArray((JsonArray)nativeRoot);
161                        }
162                        return jsonLikeRoot.getAsArray();
163                }
164                throw new DataFormatException("Content must be a valid JSON Array. It must start with '['.");
165        }
166
167        private static class GsonJsonObject extends JsonLikeObject {
168                private JsonObject nativeObject;
169                private Set<String> keySet = null;
170                private Map<String,JsonLikeValue> jsonLikeMap = new LinkedHashMap<String,JsonLikeValue>();
171                
172                public GsonJsonObject (JsonObject json) {
173                        this.nativeObject = json;
174                }
175
176                @Override
177                public Object getValue() {
178                        return null;
179                }
180
181                @Override
182                public Set<String> keySet() {
183                        if (null == keySet) {
184                                Set<Entry<String, JsonElement>> entrySet = nativeObject.entrySet();
185                                keySet = new EntryOrderedSet<String>(entrySet.size());
186                                for (Entry<String,?> entry : entrySet) {
187                                        keySet.add(entry.getKey());
188                                }
189                        }
190                        return keySet;
191                }
192
193                @Override
194                public JsonLikeValue get(String key) {
195                        JsonLikeValue result = null;
196                        if (jsonLikeMap.containsKey(key)) {
197                                result = jsonLikeMap.get(key); 
198                        } else {
199                                JsonElement child = nativeObject.get(key);
200                                if (child != null) {
201                                        result = new GsonJsonValue(child);
202                                }
203                                jsonLikeMap.put(key, result);
204                        }
205                        return result;
206                }
207        }
208        
209        private static class GsonJsonArray extends JsonLikeArray {
210                private JsonArray nativeArray;
211                private Map<Integer,JsonLikeValue> jsonLikeMap = new LinkedHashMap<Integer,JsonLikeValue>();
212                
213                public GsonJsonArray (JsonArray json) {
214                        this.nativeArray = json;
215                }
216
217                @Override
218                public Object getValue() {
219                        return null;
220                }
221
222                @Override
223                public int size() {
224                        return nativeArray.size();
225                }
226
227                @Override
228                public JsonLikeValue get(int index) {
229                        Integer key = Integer.valueOf(index);
230                        JsonLikeValue result = null;
231                        if (jsonLikeMap.containsKey(key)) {
232                                result = jsonLikeMap.get(key); 
233                        } else {
234                                JsonElement child = nativeArray.get(index);
235                                if (child != null) {
236                                        result = new GsonJsonValue(child);
237                                }
238                                jsonLikeMap.put(key, result);
239                        }
240                        return result;
241                }
242        }
243        
244        private static class GsonJsonValue extends JsonLikeValue {
245                private JsonElement nativeValue;
246                private JsonLikeObject jsonLikeObject = null;
247                private JsonLikeArray jsonLikeArray = null;
248                
249                public GsonJsonValue (JsonElement json) {
250                        this.nativeValue = json;
251                }
252
253                @Override
254                public Object getValue() {
255                        if (nativeValue != null && nativeValue.isJsonPrimitive()) {
256                                if (((JsonPrimitive)nativeValue).isNumber()) {
257                                        return nativeValue.getAsNumber();
258                                }
259                                if (((JsonPrimitive)nativeValue).isBoolean()) {
260                                        return Boolean.valueOf(nativeValue.getAsBoolean());
261                                }
262                                return nativeValue.getAsString();
263                        }
264                        return null;
265                }
266                
267                @Override
268                public ValueType getJsonType() {
269                        if (null == nativeValue || nativeValue.isJsonNull()) {
270                                return ValueType.NULL;
271                        }
272                        if (nativeValue.isJsonObject()) {
273                                return ValueType.OBJECT;
274                        }
275                        if (nativeValue.isJsonArray()) {
276                                return ValueType.ARRAY;
277                        }
278                        if (nativeValue.isJsonPrimitive()) {
279                                return ValueType.SCALAR;
280                        }
281                        return null;
282                }
283                
284                @Override
285                public ScalarType getDataType() {
286                        if (nativeValue != null && nativeValue.isJsonPrimitive()) {
287                                if (((JsonPrimitive)nativeValue).isNumber()) {
288                                        return ScalarType.NUMBER;
289                                }
290                                if (((JsonPrimitive)nativeValue).isString()) {
291                                        return ScalarType.STRING;
292                                }
293                                if (((JsonPrimitive)nativeValue).isBoolean()) {
294                                        return ScalarType.BOOLEAN;
295                                }
296                        }
297                        return null;
298                }
299
300                @Override
301                public JsonLikeArray getAsArray() {
302                        if (nativeValue != null && nativeValue.isJsonArray()) {
303                                if (null == jsonLikeArray) {
304                                        jsonLikeArray = new GsonJsonArray((JsonArray)nativeValue);
305                                }
306                        }
307                        return jsonLikeArray;
308                }
309
310                @Override
311                public JsonLikeObject getAsObject() {
312                        if (nativeValue != null && nativeValue.isJsonObject()) {
313                                if (null == jsonLikeObject) {
314                                        jsonLikeObject = new GsonJsonObject((JsonObject)nativeValue);
315                                }
316                        }
317                        return jsonLikeObject;
318                }
319
320                @Override
321                public Number getAsNumber() {
322                        return nativeValue != null ? nativeValue.getAsNumber() : null;
323                }
324
325                @Override
326                public String getAsString() {
327                        return nativeValue != null ? nativeValue.getAsString() : null;
328                }
329
330                @Override
331                public boolean getAsBoolean() {
332                        if (nativeValue != null && nativeValue.isJsonPrimitive() && ((JsonPrimitive)nativeValue).isBoolean()) {
333                                return nativeValue.getAsBoolean();
334                        }
335                        return super.getAsBoolean();
336                }
337        }
338        
339        private static class EntryOrderedSet<T> extends AbstractSet<T> {
340                private transient ArrayList<T> data = null;
341                
342                public EntryOrderedSet (int initialCapacity) {
343                        data = new ArrayList<T>(initialCapacity);
344                }
345                @SuppressWarnings("unused")
346                public EntryOrderedSet () {
347                        data = new ArrayList<T>();
348                }
349                
350                @Override
351                public int size() {
352                        return data.size();
353                }
354
355                @Override
356                public boolean contains(Object o) {
357                        return data.contains(o);
358                }
359
360                @SuppressWarnings("unused")  // not really.. just not here
361                public T get(int index) {
362                        return data.get(index);
363                }
364                
365                @Override
366                public boolean add(T element) {
367                        if (data.contains(element)) {
368                                return false;
369                        }
370                        return data.add(element);
371                }
372                
373                @Override
374                public boolean remove(Object o) {
375                        return data.remove(o);
376                }
377
378                @Override
379                public void clear() {
380                        data.clear();
381                }
382                
383                @Override
384                public Iterator<T> iterator() {
385                        return data.iterator();
386                }
387                
388        }
389}