001package ca.uhn.fhir.parser;
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.context.*;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
025import ca.uhn.fhir.model.api.ExtensionDt;
026import ca.uhn.fhir.model.api.IPrimitiveDatatype;
027import ca.uhn.fhir.model.api.IResource;
028import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
029import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
030import ca.uhn.fhir.model.api.Tag;
031import ca.uhn.fhir.model.api.TagList;
032import ca.uhn.fhir.model.api.annotation.Child;
033import ca.uhn.fhir.model.base.composite.BaseCodingDt;
034import ca.uhn.fhir.model.base.composite.BaseContainedDt;
035import ca.uhn.fhir.model.primitive.IdDt;
036import ca.uhn.fhir.model.primitive.InstantDt;
037import ca.uhn.fhir.narrative.INarrativeGenerator;
038import ca.uhn.fhir.parser.json.JsonLikeArray;
039import ca.uhn.fhir.parser.json.JsonLikeObject;
040import ca.uhn.fhir.parser.json.JsonLikeStructure;
041import ca.uhn.fhir.parser.json.JsonLikeValue;
042import ca.uhn.fhir.parser.json.JsonLikeValue.ScalarType;
043import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
044import ca.uhn.fhir.parser.json.JsonLikeWriter;
045import ca.uhn.fhir.parser.json.jackson.JacksonStructure;
046import ca.uhn.fhir.rest.api.EncodingEnum;
047import ca.uhn.fhir.util.ElementUtil;
048import ca.uhn.fhir.util.FhirTerser;
049import org.apache.commons.lang3.StringUtils;
050import org.apache.commons.lang3.Validate;
051import org.apache.commons.text.WordUtils;
052import org.hl7.fhir.instance.model.api.*;
053
054import java.io.IOException;
055import java.io.Reader;
056import java.io.Writer;
057import java.math.BigDecimal;
058import java.util.ArrayList;
059import java.util.Collections;
060import java.util.List;
061import java.util.Map;
062import java.util.Set;
063
064import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.ID_DATATYPE;
065import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_DATATYPE;
066import static org.apache.commons.lang3.StringUtils.defaultString;
067import static org.apache.commons.lang3.StringUtils.isBlank;
068import static org.apache.commons.lang3.StringUtils.isNotBlank;
069
070/**
071 * This class is the FHIR JSON parser/encoder. Users should not interact with this class directly, but should use
072 * {@link FhirContext#newJsonParser()} to get an instance.
073 */
074public class JsonParser extends BaseParser implements IJsonLikeParser {
075
076        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParser.HeldExtension.class);
077
078        private boolean myPrettyPrint;
079
080        /**
081         * Do not use this constructor, the recommended way to obtain a new instance of the JSON parser is to invoke
082         * {@link FhirContext#newJsonParser()}.
083         *
084         * @param theParserErrorHandler
085         */
086        public JsonParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler) {
087                super(theContext, theParserErrorHandler);
088        }
089
090        private boolean addToHeldComments(int valueIdx, List<String> theCommentsToAdd, ArrayList<ArrayList<String>> theListToAddTo) {
091                if (theCommentsToAdd.size() > 0) {
092                        theListToAddTo.ensureCapacity(valueIdx);
093                        while (theListToAddTo.size() <= valueIdx) {
094                                theListToAddTo.add(null);
095                        }
096                        if (theListToAddTo.get(valueIdx) == null) {
097                                theListToAddTo.set(valueIdx, new ArrayList<>());
098                        }
099                        theListToAddTo.get(valueIdx).addAll(theCommentsToAdd);
100                        return true;
101                }
102                return false;
103        }
104
105        private boolean addToHeldExtensions(int valueIdx, List<? extends IBaseExtension<?, ?>> ext, ArrayList<ArrayList<HeldExtension>> list, boolean theIsModifier, CompositeChildElement theChildElem,
106                                        CompositeChildElement theParent, EncodeContext theEncodeContext, boolean theContainedResource, IBase theContainingElement) {
107                boolean retVal = false;
108                if (ext.size() > 0) {
109                        Boolean encodeExtension = null;
110                        for (IBaseExtension<?, ?> next : ext) {
111
112                                if (next.isEmpty()) {
113                                        continue;
114                                }
115
116                                // Make sure we respect _summary and _elements
117                                if (encodeExtension == null) {
118                                        encodeExtension = isEncodeExtension(theParent, theEncodeContext, theContainedResource, theContainingElement);
119                                }
120
121                                if (encodeExtension) {
122                                        HeldExtension extension = new HeldExtension(next, theIsModifier, theChildElem, theParent);
123                                        list.ensureCapacity(valueIdx);
124                                        while (list.size() <= valueIdx) {
125                                                list.add(null);
126                                        }
127                                        ArrayList<HeldExtension> extensionList = list.get(valueIdx);
128                                        if (extensionList == null) {
129                                                extensionList = new ArrayList<>();
130                                                list.set(valueIdx, extensionList);
131                                        }
132                                        extensionList.add(extension);
133                                        retVal = true;
134                                }
135                        }
136                }
137                return retVal;
138        }
139
140        private void addToHeldIds(int theValueIdx, ArrayList<String> theListToAddTo, String theId) {
141                theListToAddTo.ensureCapacity(theValueIdx);
142                while (theListToAddTo.size() <= theValueIdx) {
143                        theListToAddTo.add(null);
144                }
145                if (theListToAddTo.get(theValueIdx) == null) {
146                        theListToAddTo.set(theValueIdx, theId);
147                }
148        }
149
150        // private void assertObjectOfType(JsonLikeValue theResourceTypeObj, Object theValueType, String thePosition) {
151        // if (theResourceTypeObj == null) {
152        // throw new DataFormatException("Invalid JSON content detected, missing required element: '" + thePosition + "'");
153        // }
154        //
155        // if (theResourceTypeObj.getValueType() != theValueType) {
156        // throw new DataFormatException("Invalid content of element " + thePosition + ", expected " + theValueType);
157        // }
158        // }
159
160        private void beginArray(JsonLikeWriter theEventWriter, String arrayName) throws IOException {
161                theEventWriter.beginArray(arrayName);
162        }
163
164        private void beginObject(JsonLikeWriter theEventWriter, String arrayName) throws IOException {
165                theEventWriter.beginObject(arrayName);
166        }
167
168        private JsonLikeWriter createJsonWriter(Writer theWriter) throws IOException {
169                JsonLikeStructure jsonStructure = new JacksonStructure();
170                return jsonStructure.getJsonLikeWriter(theWriter);
171        }
172
173        public void doEncodeResourceToJsonLikeWriter(IBaseResource theResource, JsonLikeWriter theEventWriter, EncodeContext theEncodeContext) throws IOException {
174                if (myPrettyPrint) {
175                        theEventWriter.setPrettyPrint(myPrettyPrint);
176                }
177                theEventWriter.init();
178
179                RuntimeResourceDefinition resDef = getContext().getResourceDefinition(theResource);
180                encodeResourceToJsonStreamWriter(resDef, theResource, theEventWriter, null, false, theEncodeContext);
181                theEventWriter.flush();
182        }
183
184        @Override
185        protected void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext) throws IOException {
186                JsonLikeWriter eventWriter = createJsonWriter(theWriter);
187                doEncodeResourceToJsonLikeWriter(theResource, eventWriter, theEncodeContext);
188                eventWriter.close();
189        }
190
191        @Override
192        public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) {
193                JsonLikeStructure jsonStructure = new JacksonStructure();
194                jsonStructure.load(theReader);
195
196                T retVal = doParseResource(theResourceType, jsonStructure);
197
198                return retVal;
199        }
200
201        public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, JsonLikeStructure theJsonStructure) {
202                JsonLikeObject object = theJsonStructure.getRootObject();
203
204                JsonLikeValue resourceTypeObj = object.get("resourceType");
205                if (resourceTypeObj == null || !resourceTypeObj.isString() || isBlank(resourceTypeObj.getAsString())) {
206                        throw new DataFormatException("Invalid JSON content detected, missing required element: 'resourceType'");
207                }
208
209                String resourceType = resourceTypeObj.getAsString();
210
211                ParserState<? extends IBaseResource> state = ParserState.getPreResourceInstance(this, theResourceType, getContext(), true, getErrorHandler());
212                state.enteringNewElement(null, resourceType);
213
214                parseChildren(object, state);
215
216                state.endingElement();
217                state.endingElement();
218
219                @SuppressWarnings("unchecked")
220                T retVal = (T) state.getObject();
221
222                return retVal;
223        }
224
225        private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, IBase theNextValue,
226                                                                                                                                 BaseRuntimeElementDefinition<?> theChildDef, String theChildName, boolean theContainedResource, CompositeChildElement theChildElem,
227                                                                                                                                 boolean theForceEmpty, EncodeContext theEncodeContext) throws IOException {
228
229                switch (theChildDef.getChildType()) {
230                        case ID_DATATYPE: {
231                                IIdType value = (IIdType) theNextValue;
232                                String encodedValue = "id".equals(theChildName) ? value.getIdPart() : value.getValue();
233                                if (isBlank(encodedValue)) {
234                                        break;
235                                }
236                                if (theChildName != null) {
237                                        write(theEventWriter, theChildName, encodedValue);
238                                } else {
239                                        theEventWriter.write(encodedValue);
240                                }
241                                break;
242                        }
243                        case PRIMITIVE_DATATYPE: {
244                                final IPrimitiveType<?> value = (IPrimitiveType<?>) theNextValue;
245                                final String valueStr = value.getValueAsString();
246                                if (isBlank(valueStr)) {
247                                        if (theForceEmpty) {
248                                                theEventWriter.writeNull();
249                                        }
250                                        break;
251                                }
252
253                                // check for the common case first - String value types
254                                Object valueObj = value.getValue();
255                                if (valueObj instanceof String) {
256                                        if (theChildName != null) {
257                                                theEventWriter.write(theChildName, valueStr);
258                                        } else {
259                                                theEventWriter.write(valueStr);
260                                        }
261                                        break;
262                                } else if (valueObj instanceof Long) {
263                                        if (theChildName != null) {
264                                                theEventWriter.write(theChildName, (long) valueObj);
265                                        } else {
266                                                theEventWriter.write((long) valueObj);
267                                        }
268                                        break;
269                                }
270
271                                if (value instanceof IBaseIntegerDatatype) {
272                                        if (theChildName != null) {
273                                                write(theEventWriter, theChildName, ((IBaseIntegerDatatype) value).getValue());
274                                        } else {
275                                                theEventWriter.write(((IBaseIntegerDatatype) value).getValue());
276                                        }
277                                } else if (value instanceof IBaseDecimalDatatype) {
278                                        BigDecimal decimalValue = ((IBaseDecimalDatatype) value).getValue();
279                                        decimalValue = new BigDecimal(decimalValue.toString()) {
280                                                private static final long serialVersionUID = 1L;
281
282                                                @Override
283                                                public String toString() {
284                                                        return value.getValueAsString();
285                                                }
286                                        };
287                                        if (theChildName != null) {
288                                                write(theEventWriter, theChildName, decimalValue);
289                                        } else {
290                                                theEventWriter.write(decimalValue);
291                                        }
292                                } else if (value instanceof IBaseBooleanDatatype) {
293                                        if (theChildName != null) {
294                                                write(theEventWriter, theChildName, ((IBaseBooleanDatatype) value).getValue());
295                                        } else {
296                                                Boolean booleanValue = ((IBaseBooleanDatatype) value).getValue();
297                                                if (booleanValue != null) {
298                                                        theEventWriter.write(booleanValue.booleanValue());
299                                                }
300                                        }
301                                } else {
302                                        if (theChildName != null) {
303                                                write(theEventWriter, theChildName, valueStr);
304                                        } else {
305                                                theEventWriter.write(valueStr);
306                                        }
307                                }
308                                break;
309                        }
310                        case RESOURCE_BLOCK:
311                        case COMPOSITE_DATATYPE: {
312                                if (theChildName != null) {
313                                        theEventWriter.beginObject(theChildName);
314                                } else {
315                                        theEventWriter.beginObject();
316                                }
317                                encodeCompositeElementToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theChildElem, theEncodeContext);
318                                theEventWriter.endObject();
319                                break;
320                        }
321                        case CONTAINED_RESOURCE_LIST:
322                        case CONTAINED_RESOURCES: {
323                                List<IBaseResource> containedResources = getContainedResources().getContainedResources();
324                                if (containedResources.size() > 0) {
325                                        beginArray(theEventWriter, theChildName);
326
327                                        for (IBaseResource next : containedResources) {
328                                                IIdType resourceId = getContainedResources().getResourceId(next);
329                                                String value = resourceId.getValue();
330                                                encodeResourceToJsonStreamWriter(theResDef, next, theEventWriter, null, true, fixContainedResourceId(value), theEncodeContext);
331                                        }
332
333                                        theEventWriter.endArray();
334                                }
335                                break;
336                        }
337                        case PRIMITIVE_XHTML_HL7ORG:
338                        case PRIMITIVE_XHTML: {
339                                if (!isSuppressNarratives()) {
340                                        IPrimitiveType<?> dt = (IPrimitiveType<?>) theNextValue;
341                                        if (theChildName != null) {
342                                                write(theEventWriter, theChildName, dt.getValueAsString());
343                                        } else {
344                                                theEventWriter.write(dt.getValueAsString());
345                                        }
346                                } else {
347                                        if (theChildName != null) {
348                                                // do nothing
349                                        } else {
350                                                theEventWriter.writeNull();
351                                        }
352                                }
353                                break;
354                        }
355                        case RESOURCE:
356                                IBaseResource resource = (IBaseResource) theNextValue;
357                                RuntimeResourceDefinition def = getContext().getResourceDefinition(resource);
358
359                                theEncodeContext.pushPath(def.getName(), true);
360                                encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, theContainedResource, theEncodeContext);
361                                theEncodeContext.popPath();
362
363                                break;
364                        case UNDECL_EXT:
365                        default:
366                                throw new IllegalStateException("Should not have this state here: " + theChildDef.getChildType().name());
367                }
368
369        }
370
371        private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theElement, JsonLikeWriter theEventWriter,
372                                                                                                                                                                 boolean theContainedResource, CompositeChildElement theParent, EncodeContext theEncodeContext) throws IOException {
373
374                {
375                        String elementId = getCompositeElementId(theElement);
376                        if (isNotBlank(elementId)) {
377                                write(theEventWriter, "id", elementId);
378                        }
379                }
380
381                boolean haveWrittenExtensions = false;
382                Iterable<CompositeChildElement> compositeChildElements = super.compositeChildIterator(theElement, theContainedResource, theParent, theEncodeContext);
383                for (CompositeChildElement nextChildElem : compositeChildElements) {
384
385                        BaseRuntimeChildDefinition nextChild = nextChildElem.getDef();
386
387                        if (nextChildElem.getDef().getElementName().equals("extension") || nextChildElem.getDef().getElementName().equals("modifierExtension")
388                                || nextChild instanceof RuntimeChildDeclaredExtensionDefinition) {
389                                if (!haveWrittenExtensions) {
390                                        extractAndWriteExtensionsAsDirectChild(theElement, theEventWriter, getContext().getElementDefinition(theElement.getClass()), theResDef, theResource, nextChildElem, theParent, theEncodeContext, theContainedResource);
391                                        haveWrittenExtensions = true;
392                                }
393                                continue;
394                        }
395
396                        if (nextChild instanceof RuntimeChildNarrativeDefinition) {
397                                INarrativeGenerator gen = getContext().getNarrativeGenerator();
398                                if (gen != null) {
399                                        INarrative narr;
400                                        if (theResource instanceof IResource) {
401                                                narr = ((IResource) theResource).getText();
402                                        } else if (theResource instanceof IDomainResource) {
403                                                narr = ((IDomainResource) theResource).getText();
404                                        } else {
405                                                narr = null;
406                                        }
407                                        if (narr != null && narr.isEmpty()) {
408                                                gen.populateResourceNarrative(getContext(), theResource);
409                                                if (!narr.isEmpty()) {
410                                                        RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
411                                                        String childName = nextChild.getChildNameByDatatype(child.getDatatype());
412                                                        BaseRuntimeElementDefinition<?> type = child.getChildByName(childName);
413                                                        encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, narr, type, childName, theContainedResource, nextChildElem, false, theEncodeContext);
414                                                        continue;
415                                                }
416                                        }
417                                }
418                        } else if (nextChild instanceof RuntimeChildContainedResources) {
419                                String childName = nextChild.getValidChildNames().iterator().next();
420                                BaseRuntimeElementDefinition<?> child = nextChild.getChildByName(childName);
421                                encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, null, child, childName, theContainedResource, nextChildElem, false, theEncodeContext);
422                                continue;
423                        }
424
425                        List<? extends IBase> values = nextChild.getAccessor().getValues(theElement);
426                        values = preProcessValues(nextChild, theResource, values, nextChildElem, theEncodeContext);
427
428                        if (values == null || values.isEmpty()) {
429                                continue;
430                        }
431
432                        String currentChildName = null;
433                        boolean inArray = false;
434
435                        ArrayList<ArrayList<HeldExtension>> extensions = new ArrayList<>(0);
436                        ArrayList<ArrayList<HeldExtension>> modifierExtensions = new ArrayList<>(0);
437                        ArrayList<ArrayList<String>> comments = new ArrayList<>(0);
438                        ArrayList<String> ids = new ArrayList<>(0);
439
440                        int valueIdx = 0;
441                        for (IBase nextValue : values) {
442
443                                if (nextValue == null || nextValue.isEmpty()) {
444                                        if (nextValue instanceof BaseContainedDt) {
445                                                if (theContainedResource || getContainedResources().isEmpty()) {
446                                                        continue;
447                                                }
448                                        } else {
449                                                continue;
450                                        }
451                                }
452
453                                BaseParser.ChildNameAndDef childNameAndDef = super.getChildNameAndDef(nextChild, nextValue);
454                                if (childNameAndDef == null) {
455                                        continue;
456                                }
457
458                                /*
459                                 * Often the two values below will be the same thing. There are cases though
460                                 * where they will not be. An example would be Observation.value, which is
461                                 * a choice type. If the value contains a Quantity, then:
462                                 * nextChildGenericName = "value"
463                                 * nextChildSpecificName = "valueQuantity"
464                                 */
465                                String nextChildSpecificName = childNameAndDef.getChildName();
466                                String nextChildGenericName = nextChild.getElementName();
467
468                                theEncodeContext.pushPath(nextChildGenericName, false);
469
470                                BaseRuntimeElementDefinition<?> childDef = childNameAndDef.getChildDef();
471                                boolean primitive = childDef.getChildType() == ChildTypeEnum.PRIMITIVE_DATATYPE;
472
473                                if ((childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) && theContainedResource) {
474                                        continue;
475                                }
476
477                                boolean force = false;
478                                if (primitive) {
479                                        if (nextValue instanceof ISupportsUndeclaredExtensions) {
480                                                List<ExtensionDt> ext = ((ISupportsUndeclaredExtensions) nextValue).getUndeclaredExtensions();
481                                                force |= addToHeldExtensions(valueIdx, ext, extensions, false, nextChildElem, theParent, theEncodeContext, theContainedResource, theElement);
482
483                                                ext = ((ISupportsUndeclaredExtensions) nextValue).getUndeclaredModifierExtensions();
484                                                force |= addToHeldExtensions(valueIdx, ext, modifierExtensions, true, nextChildElem, theParent, theEncodeContext, theContainedResource, theElement);
485                                        } else {
486                                                if (nextValue instanceof IBaseHasExtensions) {
487                                                        IBaseHasExtensions element = (IBaseHasExtensions) nextValue;
488                                                        List<? extends IBaseExtension<?, ?>> ext = element.getExtension();
489                                                        force |= addToHeldExtensions(valueIdx, ext, extensions, false, nextChildElem, theParent, theEncodeContext, theContainedResource, theElement);
490                                                }
491                                                if (nextValue instanceof IBaseHasModifierExtensions) {
492                                                        IBaseHasModifierExtensions element = (IBaseHasModifierExtensions) nextValue;
493                                                        List<? extends IBaseExtension<?, ?>> ext = element.getModifierExtension();
494                                                        force |= addToHeldExtensions(valueIdx, ext, modifierExtensions, true, nextChildElem, theParent, theEncodeContext, theContainedResource, theElement);
495                                                }
496                                        }
497                                        if (nextValue.hasFormatComment()) {
498                                                force |= addToHeldComments(valueIdx, nextValue.getFormatCommentsPre(), comments);
499                                                force |= addToHeldComments(valueIdx, nextValue.getFormatCommentsPost(), comments);
500                                        }
501                                        String elementId = getCompositeElementId(nextValue);
502                                        if (isNotBlank(elementId)) {
503                                                force = true;
504                                                addToHeldIds(valueIdx, ids, elementId);
505                                        }
506                                }
507
508                                if (currentChildName == null || !currentChildName.equals(nextChildSpecificName)) {
509                                        if (inArray) {
510                                                theEventWriter.endArray();
511                                        }
512                                        BaseRuntimeChildDefinition replacedParentDefinition = nextChild.getReplacedParentDefinition();
513                                        if (isMultipleCardinality(nextChild.getMax()) || (replacedParentDefinition != null && isMultipleCardinality(replacedParentDefinition.getMax()))) {
514                                                beginArray(theEventWriter, nextChildSpecificName);
515                                                inArray = true;
516                                                encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, nextChildElem, force, theEncodeContext);
517                                        } else if (nextChild instanceof RuntimeChildNarrativeDefinition && theContainedResource) {
518                                                // suppress narratives from contained resources
519                                        } else {
520                                                encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, nextChildSpecificName, theContainedResource, nextChildElem, false, theEncodeContext);
521                                        }
522                                        currentChildName = nextChildSpecificName;
523                                } else {
524                                        encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, nextChildElem, force, theEncodeContext);
525                                }
526
527                                valueIdx++;
528                                theEncodeContext.popPath();
529                        }
530
531                        if (inArray) {
532                                theEventWriter.endArray();
533                        }
534
535
536                        if (!extensions.isEmpty() || !modifierExtensions.isEmpty() || !comments.isEmpty()) {
537                                if (inArray) {
538                                        // If this is a repeatable field, the extensions go in an array too
539                                        beginArray(theEventWriter, '_' + currentChildName);
540                                } else {
541                                        beginObject(theEventWriter, '_' + currentChildName);
542                                }
543
544                                for (int i = 0; i < valueIdx; i++) {
545                                        boolean haveContent = false;
546
547                                        List<HeldExtension> heldExts = Collections.emptyList();
548                                        List<HeldExtension> heldModExts = Collections.emptyList();
549                                        if (extensions.size() > i && extensions.get(i) != null && extensions.get(i).isEmpty() == false) {
550                                                haveContent = true;
551                                                heldExts = extensions.get(i);
552                                        }
553
554                                        if (modifierExtensions.size() > i && modifierExtensions.get(i) != null && modifierExtensions.get(i).isEmpty() == false) {
555                                                haveContent = true;
556                                                heldModExts = modifierExtensions.get(i);
557                                        }
558
559                                        ArrayList<String> nextComments;
560                                        if (comments.size() > i) {
561                                                nextComments = comments.get(i);
562                                        } else {
563                                                nextComments = null;
564                                        }
565                                        if (nextComments != null && nextComments.isEmpty() == false) {
566                                                haveContent = true;
567                                        }
568
569                                        String elementId = null;
570                                        if (ids.size() > i) {
571                                                elementId = ids.get(i);
572                                                haveContent |= isNotBlank(elementId);
573                                        }
574
575                                        if (!haveContent) {
576                                                theEventWriter.writeNull();
577                                        } else {
578                                                if (inArray) {
579                                                        theEventWriter.beginObject();
580                                                }
581                                                if (isNotBlank(elementId)) {
582                                                        write(theEventWriter, "id", elementId);
583                                                }
584                                                if (nextComments != null && !nextComments.isEmpty()) {
585                                                        beginArray(theEventWriter, "fhir_comments");
586                                                        for (String next : nextComments) {
587                                                                theEventWriter.write(next);
588                                                        }
589                                                        theEventWriter.endArray();
590                                                }
591                                                writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, heldExts, heldModExts, theEncodeContext, theContainedResource);
592                                                if (inArray) {
593                                                        theEventWriter.endObject();
594                                                }
595                                        }
596                                }
597
598                                if (inArray) {
599                                        theEventWriter.endArray();
600                                } else {
601                                        theEventWriter.endObject();
602                                }
603                        }
604                }
605        }
606
607        private boolean isMultipleCardinality(int maxCardinality) {
608                return maxCardinality > 1 || maxCardinality == Child.MAX_UNLIMITED;
609        }
610
611        private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonLikeWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent, EncodeContext theEncodeContext) throws IOException, DataFormatException {
612
613                writeCommentsPreAndPost(theNextValue, theEventWriter);
614                encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theParent, theEncodeContext);
615        }
616
617        @Override
618        public void encodeResourceToJsonLikeWriter(IBaseResource theResource, JsonLikeWriter theJsonLikeWriter) throws IOException, DataFormatException {
619                Validate.notNull(theResource, "theResource can not be null");
620                Validate.notNull(theJsonLikeWriter, "theJsonLikeWriter can not be null");
621
622                if (theResource.getStructureFhirVersionEnum() != getContext().getVersion().getVersion()) {
623                        throw new IllegalArgumentException(
624                                "This parser is for FHIR version " + getContext().getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum());
625                }
626
627                EncodeContext encodeContext = new EncodeContext();
628                String resourceName = getContext().getResourceType(theResource);
629                encodeContext.pushPath(resourceName, true);
630                doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter, encodeContext);
631        }
632
633        private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull,
634                                                                                                                                 boolean theContainedResource, EncodeContext theEncodeContext) throws IOException {
635                IIdType resourceId = null;
636
637                if (StringUtils.isNotBlank(theResource.getIdElement().getIdPart())) {
638                        resourceId = theResource.getIdElement();
639                        if (theResource.getIdElement().getValue().startsWith("urn:")) {
640                                resourceId = null;
641                        }
642                }
643
644                if (!theContainedResource) {
645                        if (!super.shouldEncodeResourceId(theResource, theEncodeContext)) {
646                                resourceId = null;
647                        } else if (theEncodeContext.getResourcePath().size() == 1 && getEncodeForceResourceId() != null) {
648                                resourceId = getEncodeForceResourceId();
649                        }
650                }
651
652                encodeResourceToJsonStreamWriter(theResDef, theResource, theEventWriter, theObjectNameOrNull, theContainedResource, resourceId, theEncodeContext);
653        }
654
655        private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull,
656                                                                                                                                 boolean theContainedResource, IIdType theResourceId, EncodeContext theEncodeContext) throws IOException {
657
658                if (!super.shouldEncodeResource(theResDef.getName())) {
659                        return;
660                }
661
662                if (!theContainedResource) {
663                        setContainedResources(getContext().newTerser().containResources(theResource));
664                }
665
666                RuntimeResourceDefinition resDef = getContext().getResourceDefinition(theResource);
667
668                if (theObjectNameOrNull == null) {
669                        theEventWriter.beginObject();
670                } else {
671                        beginObject(theEventWriter, theObjectNameOrNull);
672                }
673
674                write(theEventWriter, "resourceType", resDef.getName());
675                if (theResourceId != null && theResourceId.hasIdPart()) {
676                        write(theEventWriter, "id", theResourceId.getIdPart());
677                        final List<HeldExtension> extensions = new ArrayList<>(0);
678                        final List<HeldExtension> modifierExtensions = new ArrayList<>(0);
679                        // Undeclared extensions
680                        extractUndeclaredExtensions(theResourceId, extensions, modifierExtensions, null, null, theEncodeContext, theContainedResource);
681                        boolean haveExtension = false;
682                        if (!extensions.isEmpty()) {
683                                haveExtension = true;
684                        }
685
686                        if (theResourceId.hasFormatComment() || haveExtension) {
687                                beginObject(theEventWriter, "_id");
688                                if (theResourceId.hasFormatComment()) {
689                                        writeCommentsPreAndPost(theResourceId, theEventWriter);
690                                }
691                                if (haveExtension) {
692                                        writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions, theEncodeContext, theContainedResource);
693                                }
694                                theEventWriter.endObject();
695                        }
696                }
697
698                if (theResource instanceof IResource) {
699                        IResource resource = (IResource) theResource;
700                        // Object securityLabelRawObj =
701
702                        List<BaseCodingDt> securityLabels = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.SECURITY_LABELS);
703                        List<? extends IIdType> profiles = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.PROFILES);
704                        profiles = super.getProfileTagsForEncoding(resource, profiles);
705
706                        TagList tags = getMetaTagsForEncoding(resource, theEncodeContext);
707                        InstantDt updated = (InstantDt) resource.getResourceMetadata().get(ResourceMetadataKeyEnum.UPDATED);
708                        IdDt resourceId = resource.getId();
709                        String versionIdPart = resourceId.getVersionIdPart();
710                        if (isBlank(versionIdPart)) {
711                                versionIdPart = ResourceMetadataKeyEnum.VERSION.get(resource);
712                        }
713                        List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> extensionMetadataKeys = getExtensionMetadataKeys(resource);
714
715                        if (super.shouldEncodeResourceMeta(resource) && (ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles) == false) || !extensionMetadataKeys.isEmpty()) {
716                                beginObject(theEventWriter, "meta");
717
718                                if (shouldEncodePath(resource, "meta.versionId")) {
719                                        writeOptionalTagWithTextNode(theEventWriter, "versionId", versionIdPart);
720                                }
721                                if (shouldEncodePath(resource, "meta.lastUpdated")) {
722                                        writeOptionalTagWithTextNode(theEventWriter, "lastUpdated", updated);
723                                }
724
725                                if (profiles != null && profiles.isEmpty() == false) {
726                                        beginArray(theEventWriter, "profile");
727                                        for (IIdType profile : profiles) {
728                                                if (profile != null && isNotBlank(profile.getValue())) {
729                                                        theEventWriter.write(profile.getValue());
730                                                }
731                                        }
732                                        theEventWriter.endArray();
733                                }
734
735                                if (securityLabels.isEmpty() == false) {
736                                        beginArray(theEventWriter, "security");
737                                        for (BaseCodingDt securityLabel : securityLabels) {
738                                                theEventWriter.beginObject();
739                                                theEncodeContext.pushPath("security", false);
740                                                encodeCompositeElementChildrenToStreamWriter(resDef, resource, securityLabel, theEventWriter, theContainedResource, null, theEncodeContext);
741                                                theEncodeContext.popPath();
742                                                theEventWriter.endObject();
743                                        }
744                                        theEventWriter.endArray();
745                                }
746
747                                if (tags != null && tags.isEmpty() == false) {
748                                        beginArray(theEventWriter, "tag");
749                                        for (Tag tag : tags) {
750                                                if (tag.isEmpty()) {
751                                                        continue;
752                                                }
753                                                theEventWriter.beginObject();
754                                                writeOptionalTagWithTextNode(theEventWriter, "system", tag.getScheme());
755                                                writeOptionalTagWithTextNode(theEventWriter, "code", tag.getTerm());
756                                                writeOptionalTagWithTextNode(theEventWriter, "display", tag.getLabel());
757                                                theEventWriter.endObject();
758                                        }
759                                        theEventWriter.endArray();
760                                }
761
762                                addExtensionMetadata(theResDef, theResource, theContainedResource, extensionMetadataKeys, resDef, theEventWriter, theEncodeContext);
763
764                                theEventWriter.endObject(); // end meta
765                        }
766                }
767
768                encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, new CompositeChildElement(resDef, theEncodeContext), theEncodeContext);
769
770                theEventWriter.endObject();
771        }
772
773
774        private void addExtensionMetadata(RuntimeResourceDefinition theResDef, IBaseResource theResource,
775                                                                                                 boolean theContainedResource,
776                                                                                                 List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> extensionMetadataKeys,
777                                                                                                 RuntimeResourceDefinition resDef,
778                                                                                                 JsonLikeWriter theEventWriter, EncodeContext theEncodeContext) throws IOException {
779                if (extensionMetadataKeys.isEmpty()) {
780                        return;
781                }
782
783                ExtensionDt metaResource = new ExtensionDt();
784                for (Map.Entry<ResourceMetadataKeyEnum<?>, Object> entry : extensionMetadataKeys) {
785                        metaResource.addUndeclaredExtension((ExtensionDt) entry.getValue());
786                }
787                encodeCompositeElementToStreamWriter(theResDef, theResource, metaResource, theEventWriter, theContainedResource, new CompositeChildElement(resDef, theEncodeContext), theEncodeContext);
788        }
789
790        /**
791         * This is useful only for the two cases where extensions are encoded as direct children (e.g. not in some object
792         * called _name): resource extensions, and extension extensions
793         */
794        private void extractAndWriteExtensionsAsDirectChild(IBase theElement, JsonLikeWriter theEventWriter, BaseRuntimeElementDefinition<?> theElementDef, RuntimeResourceDefinition theResDef,
795                                                                                                                                                 IBaseResource theResource, CompositeChildElement theChildElem, CompositeChildElement theParent, EncodeContext theEncodeContext, boolean theContainedResource) throws IOException {
796                List<HeldExtension> extensions = new ArrayList<>(0);
797                List<HeldExtension> modifierExtensions = new ArrayList<>(0);
798
799                // Undeclared extensions
800                extractUndeclaredExtensions(theElement, extensions, modifierExtensions, theChildElem, theParent, theEncodeContext, theContainedResource);
801
802                // Declared extensions
803                if (theElementDef != null) {
804                        extractDeclaredExtensions(theElement, theElementDef, extensions, modifierExtensions, theChildElem);
805                }
806
807                // Write the extensions
808                writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions, theEncodeContext, theContainedResource);
809        }
810
811        private void extractDeclaredExtensions(IBase theResource, BaseRuntimeElementDefinition<?> resDef, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions,
812                                                                                                                CompositeChildElement theChildElem) {
813                for (RuntimeChildDeclaredExtensionDefinition nextDef : resDef.getExtensionsNonModifier()) {
814                        for (IBase nextValue : nextDef.getAccessor().getValues(theResource)) {
815                                if (nextValue != null) {
816                                        if (nextValue.isEmpty()) {
817                                                continue;
818                                        }
819                                        extensions.add(new HeldExtension(nextDef, nextValue, theChildElem));
820                                }
821                        }
822                }
823                for (RuntimeChildDeclaredExtensionDefinition nextDef : resDef.getExtensionsModifier()) {
824                        for (IBase nextValue : nextDef.getAccessor().getValues(theResource)) {
825                                if (nextValue != null) {
826                                        if (nextValue.isEmpty()) {
827                                                continue;
828                                        }
829                                        modifierExtensions.add(new HeldExtension(nextDef, nextValue, theChildElem));
830                                }
831                        }
832                }
833        }
834
835        private void extractUndeclaredExtensions(IBase theElement, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions, CompositeChildElement theChildElem,
836                                                                                                                  CompositeChildElement theParent, EncodeContext theEncodeContext, boolean theContainedResource) {
837                if (theElement instanceof ISupportsUndeclaredExtensions) {
838                        ISupportsUndeclaredExtensions element = (ISupportsUndeclaredExtensions) theElement;
839                        List<ExtensionDt> ext = element.getUndeclaredExtensions();
840                        for (ExtensionDt next : ext) {
841                                if (next == null || next.isEmpty()) {
842                                        continue;
843                                }
844                                extensions.add(new HeldExtension(next, false, theChildElem, theParent));
845                        }
846
847                        ext = element.getUndeclaredModifierExtensions();
848                        for (ExtensionDt next : ext) {
849                                if (next == null || next.isEmpty()) {
850                                        continue;
851                                }
852                                modifierExtensions.add(new HeldExtension(next, true, theChildElem, theParent));
853                        }
854                } else {
855                        if (theElement instanceof IBaseHasExtensions) {
856                                IBaseHasExtensions element = (IBaseHasExtensions) theElement;
857                                List<? extends IBaseExtension<?, ?>> ext = element.getExtension();
858                                Boolean encodeExtension = null;
859                                for (IBaseExtension<?, ?> next : ext) {
860                                        if (next == null || (ElementUtil.isEmpty(next.getValue()) && next.getExtension().isEmpty())) {
861                                                continue;
862                                        }
863
864                                        // Make sure we respect _elements and _summary
865                                        if (encodeExtension == null) {
866                                                encodeExtension = isEncodeExtension(theParent, theEncodeContext, theContainedResource, element);
867                                        }
868                                        if (encodeExtension) {
869                                                HeldExtension extension = new HeldExtension(next, false, theChildElem, theParent);
870                                                extensions.add(extension);
871                                        }
872
873                                }
874                        }
875                        if (theElement instanceof IBaseHasModifierExtensions) {
876                                IBaseHasModifierExtensions element = (IBaseHasModifierExtensions) theElement;
877                                List<? extends IBaseExtension<?, ?>> ext = element.getModifierExtension();
878                                for (IBaseExtension<?, ?> next : ext) {
879                                        if (next == null || next.isEmpty()) {
880                                                continue;
881                                        }
882
883                                        HeldExtension extension = new HeldExtension(next, true, theChildElem, theParent);
884                                        modifierExtensions.add(extension);
885                                }
886                        }
887                }
888        }
889
890        private boolean isEncodeExtension(CompositeChildElement theParent, EncodeContext theEncodeContext, boolean theContainedResource, IBase theElement) {
891                BaseRuntimeElementDefinition<?> runtimeElementDefinition = getContext().getElementDefinition(theElement.getClass());
892                boolean retVal = true;
893                if (runtimeElementDefinition instanceof BaseRuntimeElementCompositeDefinition) {
894                        BaseRuntimeElementCompositeDefinition definition = (BaseRuntimeElementCompositeDefinition) runtimeElementDefinition;
895                        BaseRuntimeChildDefinition childDef = definition.getChildByName("extension");
896                        CompositeChildElement c = new CompositeChildElement(theParent, childDef, theEncodeContext);
897                        retVal = c.shouldBeEncoded(theContainedResource);
898                }
899                return retVal;
900        }
901
902        @Override
903        public EncodingEnum getEncoding() {
904                return EncodingEnum.JSON;
905        }
906
907        private JsonLikeArray grabJsonArray(JsonLikeObject theObject, String nextName, String thePosition) {
908                JsonLikeValue object = theObject.get(nextName);
909                if (object == null || object.isNull()) {
910                        return null;
911                }
912                if (!object.isArray()) {
913                        throw new DataFormatException("Syntax error parsing JSON FHIR structure: Expected ARRAY at element '" + thePosition + "', found '" + object.getJsonType() + "'");
914                }
915                return object.getAsArray();
916        }
917
918        private void parseAlternates(JsonLikeValue theAlternateVal, ParserState<?> theState, String theElementName, String theAlternateName) {
919                if (theAlternateVal == null || theAlternateVal.isNull()) {
920                        return;
921                }
922
923                if (theAlternateVal.isArray()) {
924                        JsonLikeArray array = theAlternateVal.getAsArray();
925                        if (array.size() > 1) {
926                                throw new DataFormatException("Unexpected array of length " + array.size() + " (expected 0 or 1) for element: " + theElementName);
927                        }
928                        if (array.size() == 0) {
929                                return;
930                        }
931                        parseAlternates(array.get(0), theState, theElementName, theAlternateName);
932                        return;
933                }
934
935                JsonLikeValue alternateVal = theAlternateVal;
936                if (alternateVal.isObject() == false) {
937                        getErrorHandler().incorrectJsonType(null, theAlternateName, ValueType.OBJECT, null, alternateVal.getJsonType(), null);
938                        return;
939                }
940
941                JsonLikeObject alternate = alternateVal.getAsObject();
942                for (String nextKey : alternate.keySet()) {
943                        JsonLikeValue nextVal = alternate.get(nextKey);
944                        if ("extension".equals(nextKey)) {
945                                boolean isModifier = false;
946                                JsonLikeArray array = nextVal.getAsArray();
947                                parseExtension(theState, array, isModifier);
948                        } else if ("modifierExtension".equals(nextKey)) {
949                                boolean isModifier = true;
950                                JsonLikeArray array = nextVal.getAsArray();
951                                parseExtension(theState, array, isModifier);
952                        } else if ("id".equals(nextKey)) {
953                                if (nextVal.isString()) {
954                                        theState.attributeValue("id", nextVal.getAsString());
955                                } else {
956                                        getErrorHandler().incorrectJsonType(null, "id", ValueType.SCALAR, ScalarType.STRING, nextVal.getJsonType(), nextVal.getDataType());
957                                }
958                        } else if ("fhir_comments".equals(nextKey)) {
959                                parseFhirComments(nextVal, theState);
960                        }
961                }
962        }
963
964        private void parseChildren(JsonLikeObject theObject, ParserState<?> theState) {
965                Set<String> keySet = theObject.keySet();
966
967                int allUnderscoreNames = 0;
968                int handledUnderscoreNames = 0;
969
970                for (String nextName : keySet) {
971                        if ("resourceType".equals(nextName)) {
972                                continue;
973                        } else if ("extension".equals(nextName)) {
974                                JsonLikeArray array = grabJsonArray(theObject, nextName, "extension");
975                                parseExtension(theState, array, false);
976                                continue;
977                        } else if ("modifierExtension".equals(nextName)) {
978                                JsonLikeArray array = grabJsonArray(theObject, nextName, "modifierExtension");
979                                parseExtension(theState, array, true);
980                                continue;
981                        } else if (nextName.equals("fhir_comments")) {
982                                parseFhirComments(theObject.get(nextName), theState);
983                                continue;
984                        } else if (nextName.charAt(0) == '_') {
985                                allUnderscoreNames++;
986                                continue;
987                        }
988
989                        JsonLikeValue nextVal = theObject.get(nextName);
990                        String alternateName = '_' + nextName;
991                        JsonLikeValue alternateVal = theObject.get(alternateName);
992                        if (alternateVal != null) {
993                                handledUnderscoreNames++;
994                        }
995
996                        parseChildren(theState, nextName, nextVal, alternateVal, alternateName, false);
997
998                }
999
1000                // if (elementId != null) {
1001                // IBase object = (IBase) theState.getObject();
1002                // if (object instanceof IIdentifiableElement) {
1003                // ((IIdentifiableElement) object).setElementSpecificId(elementId);
1004                // } else if (object instanceof IBaseResource) {
1005                // ((IBaseResource) object).getIdElement().setValue(elementId);
1006                // }
1007                // }
1008
1009                /*
1010                 * This happens if an element has an extension but no actual value. I.e.
1011                 * if a resource has a "_status" element but no corresponding "status"
1012                 * element. This could be used to handle a null value with an extension
1013                 * for example.
1014                 */
1015                if (allUnderscoreNames > handledUnderscoreNames) {
1016                        for (String alternateName : keySet) {
1017                                if (alternateName.startsWith("_") && alternateName.length() > 1) {
1018                                        JsonLikeValue nextValue = theObject.get(alternateName);
1019                                        if (nextValue != null) {
1020                                                if (nextValue.isObject()) {
1021                                                        String nextName = alternateName.substring(1);
1022                                                        if (theObject.get(nextName) == null) {
1023                                                                theState.enteringNewElement(null, nextName);
1024                                                                parseAlternates(nextValue, theState, alternateName, alternateName);
1025                                                                theState.endingElement();
1026                                                        }
1027                                                } else {
1028                                                        getErrorHandler().incorrectJsonType(null, alternateName, ValueType.OBJECT, null, nextValue.getJsonType(), null);
1029                                                }
1030                                        }
1031                                }
1032                        }
1033                }
1034
1035        }
1036
1037        private void parseChildren(ParserState<?> theState, String theName, JsonLikeValue theJsonVal, JsonLikeValue theAlternateVal, String theAlternateName, boolean theInArray) {
1038                if (theName.equals("id")) {
1039                        if (!theJsonVal.isString()) {
1040                                getErrorHandler().incorrectJsonType(null, "id", ValueType.SCALAR, ScalarType.STRING, theJsonVal.getJsonType(), theJsonVal.getDataType());
1041                        }
1042                }
1043
1044                if (theJsonVal.isArray()) {
1045                        JsonLikeArray nextArray = theJsonVal.getAsArray();
1046
1047                        JsonLikeValue alternateVal = theAlternateVal;
1048                        if (alternateVal != null && alternateVal.isArray() == false) {
1049                                getErrorHandler().incorrectJsonType(null, theAlternateName, ValueType.ARRAY, null, alternateVal.getJsonType(), null);
1050                                alternateVal = null;
1051                        }
1052
1053                        JsonLikeArray nextAlternateArray = JsonLikeValue.asArray(alternateVal); // could be null
1054                        for (int i = 0; i < nextArray.size(); i++) {
1055                                JsonLikeValue nextObject = nextArray.get(i);
1056                                JsonLikeValue nextAlternate = null;
1057                                if (nextAlternateArray != null && nextAlternateArray.size() >= (i + 1)) {
1058                                        nextAlternate = nextAlternateArray.get(i);
1059                                }
1060                                parseChildren(theState, theName, nextObject, nextAlternate, theAlternateName, true);
1061                        }
1062                } else if (theJsonVal.isObject()) {
1063                        if (!theInArray && theState.elementIsRepeating(theName)) {
1064                                getErrorHandler().incorrectJsonType(null, theName, ValueType.ARRAY, null, ValueType.OBJECT, null);
1065                        }
1066
1067                        theState.enteringNewElement(null, theName);
1068                        parseAlternates(theAlternateVal, theState, theAlternateName, theAlternateName);
1069                        JsonLikeObject nextObject = theJsonVal.getAsObject();
1070                        boolean preResource = false;
1071                        if (theState.isPreResource()) {
1072                                JsonLikeValue resType = nextObject.get("resourceType");
1073                                if (resType == null || !resType.isString()) {
1074                                        throw new DataFormatException("Missing required element 'resourceType' from JSON resource object, unable to parse");
1075                                }
1076                                theState.enteringNewElement(null, resType.getAsString());
1077                                preResource = true;
1078                        }
1079                        parseChildren(nextObject, theState);
1080                        if (preResource) {
1081                                theState.endingElement();
1082                        }
1083                        theState.endingElement();
1084                } else if (theJsonVal.isNull()) {
1085                        theState.enteringNewElement(null, theName);
1086                        parseAlternates(theAlternateVal, theState, theAlternateName, theAlternateName);
1087                        theState.endingElement();
1088                } else {
1089                        // must be a SCALAR
1090                        theState.enteringNewElement(null, theName);
1091                        String asString = theJsonVal.getAsString();
1092                        theState.attributeValue("value", asString);
1093                        parseAlternates(theAlternateVal, theState, theAlternateName, theAlternateName);
1094                        theState.endingElement();
1095                }
1096        }
1097
1098        private void parseExtension(ParserState<?> theState, JsonLikeArray theValues, boolean theIsModifier) {
1099                int allUnderscoreNames = 0;
1100                int handledUnderscoreNames = 0;
1101
1102                for (int i = 0; i < theValues.size(); i++) {
1103                        JsonLikeObject nextExtObj = JsonLikeValue.asObject(theValues.get(i));
1104                        JsonLikeValue jsonElement = nextExtObj.get("url");
1105                        String url;
1106                        if (null == jsonElement || !(jsonElement.isScalar())) {
1107                                String parentElementName;
1108                                if (theIsModifier) {
1109                                        parentElementName = "modifierExtension";
1110                                } else {
1111                                        parentElementName = "extension";
1112                                }
1113                                getErrorHandler().missingRequiredElement(new ParseLocation().setParentElementName(parentElementName), "url");
1114                                url = null;
1115                        } else {
1116                                url = getExtensionUrl(jsonElement.getAsString());
1117                        }
1118                        theState.enteringNewElementExtension(null, url, theIsModifier, getServerBaseUrl());
1119                        for (String next : nextExtObj.keySet()) {
1120                                if ("url".equals(next)) {
1121                                        continue;
1122                                } else if ("extension".equals(next)) {
1123                                        JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
1124                                        parseExtension(theState, jsonVal, false);
1125                                } else if ("modifierExtension".equals(next)) {
1126                                        JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
1127                                        parseExtension(theState, jsonVal, true);
1128                                } else if (next.charAt(0) == '_') {
1129                                        allUnderscoreNames++;
1130                                        continue;
1131                                } else {
1132                                        JsonLikeValue jsonVal = nextExtObj.get(next);
1133                                        String alternateName = '_' + next;
1134                                        JsonLikeValue alternateVal = nextExtObj.get(alternateName);
1135                                        if (alternateVal != null) {
1136                                                handledUnderscoreNames++;
1137                                        }
1138                                        parseChildren(theState, next, jsonVal, alternateVal, alternateName, false);
1139                                }
1140                        }
1141
1142                        /*
1143                         * This happens if an element has an extension but no actual value. I.e.
1144                         * if a resource has a "_status" element but no corresponding "status"
1145                         * element. This could be used to handle a null value with an extension
1146                         * for example.
1147                         */
1148                        if (allUnderscoreNames > handledUnderscoreNames) {
1149                                for (String alternateName : nextExtObj.keySet()) {
1150                                        if (alternateName.startsWith("_") && alternateName.length() > 1) {
1151                                                JsonLikeValue nextValue = nextExtObj.get(alternateName);
1152                                                if (nextValue != null) {
1153                                                        if (nextValue.isObject()) {
1154                                                                String nextName = alternateName.substring(1);
1155                                                                if (nextExtObj.get(nextName) == null) {
1156                                                                        theState.enteringNewElement(null, nextName);
1157                                                                        parseAlternates(nextValue, theState, alternateName, alternateName);
1158                                                                        theState.endingElement();
1159                                                                }
1160                                                        } else {
1161                                                                getErrorHandler().incorrectJsonType(null, alternateName, ValueType.OBJECT, null, nextValue.getJsonType(), null);
1162                                                        }
1163                                                }
1164                                        }
1165                                }
1166                        }
1167                        theState.endingElement();
1168                }
1169        }
1170
1171        private void parseFhirComments(JsonLikeValue theObject, ParserState<?> theState) {
1172                if (theObject.isArray()) {
1173                        JsonLikeArray comments = theObject.getAsArray();
1174                        for (int i = 0; i < comments.size(); i++) {
1175                                JsonLikeValue nextComment = comments.get(i);
1176                                if (nextComment.isString()) {
1177                                        String commentText = nextComment.getAsString();
1178                                        if (commentText != null) {
1179                                                theState.commentPre(commentText);
1180                                        }
1181                                }
1182                        }
1183                }
1184        }
1185
1186        @Override
1187        public <T extends IBaseResource> T parseResource(Class<T> theResourceType, JsonLikeStructure theJsonLikeStructure) throws DataFormatException {
1188
1189                /*****************************************************
1190                 * ************************************************* *
1191                 * ** NOTE: this duplicates most of the code in ** *
1192                 * ** BaseParser.parseResource(Class<T>, Reader). ** *
1193                 * ** Unfortunately, there is no way to avoid ** *
1194                 * ** this without doing some refactoring of the ** *
1195                 * ** BaseParser class. ** *
1196                 * ************************************************* *
1197                 *****************************************************/
1198
1199                /*
1200                 * We do this so that the context can verify that the structure is for
1201                 * the correct FHIR version
1202                 */
1203                if (theResourceType != null) {
1204                        getContext().getResourceDefinition(theResourceType);
1205                }
1206
1207                // Actually do the parse
1208                T retVal = doParseResource(theResourceType, theJsonLikeStructure);
1209
1210                RuntimeResourceDefinition def = getContext().getResourceDefinition(retVal);
1211                if ("Bundle".equals(def.getName())) {
1212
1213                        BaseRuntimeChildDefinition entryChild = def.getChildByName("entry");
1214                        BaseRuntimeElementCompositeDefinition<?> entryDef = (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry");
1215                        List<IBase> entries = entryChild.getAccessor().getValues(retVal);
1216                        if (entries != null) {
1217                                for (IBase nextEntry : entries) {
1218
1219                                        /**
1220                                         * If Bundle.entry.fullUrl is populated, set the resource ID to that
1221                                         */
1222                                        // TODO: should emit a warning and maybe notify the error handler if the resource ID doesn't match the
1223                                        // fullUrl idPart
1224                                        BaseRuntimeChildDefinition fullUrlChild = entryDef.getChildByName("fullUrl");
1225                                        if (fullUrlChild == null) {
1226                                                continue; // TODO: remove this once the data model in tinder plugin catches up to 1.2
1227                                        }
1228                                        List<IBase> fullUrl = fullUrlChild.getAccessor().getValues(nextEntry);
1229                                        if (fullUrl != null && !fullUrl.isEmpty()) {
1230                                                IPrimitiveType<?> value = (IPrimitiveType<?>) fullUrl.get(0);
1231                                                if (value.isEmpty() == false) {
1232                                                        List<IBase> entryResources = entryDef.getChildByName("resource").getAccessor().getValues(nextEntry);
1233                                                        if (entryResources != null && entryResources.size() > 0) {
1234                                                                IBaseResource res = (IBaseResource) entryResources.get(0);
1235                                                                String versionId = res.getIdElement().getVersionIdPart();
1236                                                                res.setId(value.getValueAsString());
1237                                                                if (isNotBlank(versionId) && res.getIdElement().hasVersionIdPart() == false) {
1238                                                                        res.setId(res.getIdElement().withVersion(versionId));
1239                                                                }
1240                                                        }
1241                                                }
1242                                        }
1243
1244                                }
1245                        }
1246
1247                }
1248
1249                return retVal;
1250        }
1251
1252        @Override
1253        public IBaseResource parseResource(JsonLikeStructure theJsonLikeStructure) throws DataFormatException {
1254                return parseResource(null, theJsonLikeStructure);
1255        }
1256
1257        @Override
1258        public IParser setPrettyPrint(boolean thePrettyPrint) {
1259                myPrettyPrint = thePrettyPrint;
1260                return this;
1261        }
1262
1263        private void write(JsonLikeWriter theEventWriter, String theChildName, Boolean theValue) throws IOException {
1264                if (theValue != null) {
1265                        theEventWriter.write(theChildName, theValue.booleanValue());
1266                }
1267        }
1268
1269        // private void parseExtensionInDstu2Style(boolean theModifier, ParserState<?> theState, String
1270        // theParentExtensionUrl, String theExtensionUrl, JsonArray theValues) {
1271        // String extUrl = UrlUtil.constructAbsoluteUrl(theParentExtensionUrl, theExtensionUrl);
1272        // theState.enteringNewElementExtension(null, extUrl, theModifier);
1273        //
1274        // for (int extIdx = 0; extIdx < theValues.size(); extIdx++) {
1275        // JsonObject nextExt = theValues.getJsonObject(extIdx);
1276        // for (String nextKey : nextExt.keySet()) {
1277        // // if (nextKey.startsWith("value") && nextKey.length() > 5 &&
1278        // // myContext.getRuntimeChildUndeclaredExtensionDefinition().getChildByName(nextKey) != null) {
1279        // JsonElement jsonVal = nextExt.get(nextKey);
1280        // if (jsonVal.getValueType() == ValueType.ARRAY) {
1281        // /*
1282        // * Extension children which are arrays are sub-extensions. Any other value type should be treated as a value.
1283        // */
1284        // JsonArray arrayValue = (JsonArray) jsonVal;
1285        // parseExtensionInDstu2Style(theModifier, theState, extUrl, nextKey, arrayValue);
1286        // } else {
1287        // parseChildren(theState, nextKey, jsonVal, null, null);
1288        // }
1289        // }
1290        // }
1291        //
1292        // theState.endingElement();
1293        // }
1294
1295        private void write(JsonLikeWriter theEventWriter, String theChildName, BigDecimal theDecimalValue) throws IOException {
1296                theEventWriter.write(theChildName, theDecimalValue);
1297        }
1298
1299        private void write(JsonLikeWriter theEventWriter, String theChildName, Integer theValue) throws IOException {
1300                theEventWriter.write(theChildName, theValue);
1301        }
1302
1303        private void writeCommentsPreAndPost(IBase theNextValue, JsonLikeWriter theEventWriter) throws IOException {
1304                if (theNextValue.hasFormatComment()) {
1305                        beginArray(theEventWriter, "fhir_comments");
1306                        List<String> pre = theNextValue.getFormatCommentsPre();
1307                        if (pre.isEmpty() == false) {
1308                                for (String next : pre) {
1309                                        theEventWriter.write(next);
1310                                }
1311                        }
1312                        List<String> post = theNextValue.getFormatCommentsPost();
1313                        if (post.isEmpty() == false) {
1314                                for (String next : post) {
1315                                        theEventWriter.write(next);
1316                                }
1317                        }
1318                        theEventWriter.endArray();
1319                }
1320        }
1321
1322        private void writeExtensionsAsDirectChild(IBaseResource theResource, JsonLikeWriter theEventWriter, RuntimeResourceDefinition resDef, List<HeldExtension> extensions,
1323                                                                                                                        List<HeldExtension> modifierExtensions, EncodeContext theEncodeContext, boolean theContainedResource) throws IOException {
1324                // Write Extensions
1325                if (extensions.isEmpty() == false) {
1326                        theEncodeContext.pushPath("extension", false);
1327                        beginArray(theEventWriter, "extension");
1328                        for (HeldExtension next : extensions) {
1329                                next.write(resDef, theResource, theEventWriter, theEncodeContext, theContainedResource);
1330                        }
1331                        theEventWriter.endArray();
1332                        theEncodeContext.popPath();
1333                }
1334
1335                // Write ModifierExtensions
1336                if (modifierExtensions.isEmpty() == false) {
1337                        theEncodeContext.pushPath("modifierExtension", false);
1338                        beginArray(theEventWriter, "modifierExtension");
1339                        for (HeldExtension next : modifierExtensions) {
1340                                next.write(resDef, theResource, theEventWriter, theEncodeContext, theContainedResource);
1341                        }
1342                        theEventWriter.endArray();
1343                        theEncodeContext.popPath();
1344                }
1345        }
1346
1347        private void writeOptionalTagWithTextNode(JsonLikeWriter theEventWriter, String theElementName, IPrimitiveDatatype<?> thePrimitive) throws IOException {
1348                if (thePrimitive == null) {
1349                        return;
1350                }
1351                String str = thePrimitive.getValueAsString();
1352                writeOptionalTagWithTextNode(theEventWriter, theElementName, str);
1353        }
1354
1355        private void writeOptionalTagWithTextNode(JsonLikeWriter theEventWriter, String theElementName, String theValue) throws IOException {
1356                if (StringUtils.isNotBlank(theValue)) {
1357                        write(theEventWriter, theElementName, theValue);
1358                }
1359        }
1360
1361        private class HeldExtension implements Comparable<HeldExtension> {
1362
1363                private CompositeChildElement myChildElem;
1364                private RuntimeChildDeclaredExtensionDefinition myDef;
1365                private boolean myModifier;
1366                private IBaseExtension<?, ?> myUndeclaredExtension;
1367                private IBase myValue;
1368                private CompositeChildElement myParent;
1369
1370                public HeldExtension(IBaseExtension<?, ?> theUndeclaredExtension, boolean theModifier, CompositeChildElement theChildElem, CompositeChildElement theParent) {
1371                        assert theUndeclaredExtension != null;
1372                        myUndeclaredExtension = theUndeclaredExtension;
1373                        myModifier = theModifier;
1374                        myChildElem = theChildElem;
1375                        myParent = theParent;
1376                }
1377
1378                public HeldExtension(RuntimeChildDeclaredExtensionDefinition theDef, IBase theValue, CompositeChildElement theChildElem) {
1379                        assert theDef != null;
1380                        assert theValue != null;
1381                        myDef = theDef;
1382                        myValue = theValue;
1383                        myChildElem = theChildElem;
1384                }
1385
1386                @Override
1387                public int compareTo(HeldExtension theArg0) {
1388                        String url1 = myDef != null ? myDef.getExtensionUrl() : myUndeclaredExtension.getUrl();
1389                        String url2 = theArg0.myDef != null ? theArg0.myDef.getExtensionUrl() : theArg0.myUndeclaredExtension.getUrl();
1390                        url1 = defaultString(getExtensionUrl(url1));
1391                        url2 = defaultString(getExtensionUrl(url2));
1392                        return url1.compareTo(url2);
1393                }
1394
1395                private void managePrimitiveExtension(final IBase theValue, final RuntimeResourceDefinition theResDef, final IBaseResource theResource, final JsonLikeWriter theEventWriter, final BaseRuntimeElementDefinition<?> def, final String childName, EncodeContext theEncodeContext, boolean theContainedResource) throws IOException {
1396                        if (def.getChildType().equals(ID_DATATYPE) || def.getChildType().equals(PRIMITIVE_DATATYPE)) {
1397                                final List<HeldExtension> extensions = new ArrayList<HeldExtension>(0);
1398                                final List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0);
1399                                // Undeclared extensions
1400                                extractUndeclaredExtensions(theValue, extensions, modifierExtensions, myParent, null, theEncodeContext, theContainedResource);
1401                                // Declared extensions
1402                                if (def != null) {
1403                                        extractDeclaredExtensions(theValue, def, extensions, modifierExtensions, myParent);
1404                                }
1405                                boolean haveContent = false;
1406                                if (!extensions.isEmpty() || !modifierExtensions.isEmpty()) {
1407                                        haveContent = true;
1408                                }
1409                                if (haveContent) {
1410                                        beginObject(theEventWriter, '_' + childName);
1411                                        writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions, theEncodeContext, theContainedResource);
1412                                        theEventWriter.endObject();
1413                                }
1414                        }
1415                }
1416
1417                public void write(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, EncodeContext theEncodeContext, boolean theContainedResource) throws IOException {
1418                        if (myUndeclaredExtension != null) {
1419                                writeUndeclaredExtension(theResDef, theResource, theEventWriter, myUndeclaredExtension, theEncodeContext, theContainedResource);
1420                        } else {
1421                                theEventWriter.beginObject();
1422
1423                                writeCommentsPreAndPost(myValue, theEventWriter);
1424
1425                                JsonParser.write(theEventWriter, "url", getExtensionUrl(myDef.getExtensionUrl()));
1426
1427                                /*
1428                                 * This makes sure that even if the extension contains a reference to a contained
1429                                 * resource which has a HAPI-assigned ID we'll still encode that ID.
1430                                 *
1431                                 * See #327
1432                                 */
1433                                List<? extends IBase> preProcessedValue = preProcessValues(myDef, theResource, Collections.singletonList(myValue), myChildElem, theEncodeContext);
1434
1435                                // // Check for undeclared extensions on the declared extension
1436                                // // (grrrrrr....)
1437                                // if (myValue instanceof ISupportsUndeclaredExtensions) {
1438                                // ISupportsUndeclaredExtensions value = (ISupportsUndeclaredExtensions)myValue;
1439                                // List<ExtensionDt> exts = value.getUndeclaredExtensions();
1440                                // if (exts.size() > 0) {
1441                                // ArrayList<IBase> newValueList = new ArrayList<IBase>();
1442                                // newValueList.addAll(preProcessedValue);
1443                                // newValueList.addAll(exts);
1444                                // preProcessedValue = newValueList;
1445                                // }
1446                                // }
1447
1448                                myValue = preProcessedValue.get(0);
1449
1450                                BaseRuntimeElementDefinition<?> def = myDef.getChildElementDefinitionByDatatype(myValue.getClass());
1451                                if (def.getChildType() == ChildTypeEnum.RESOURCE_BLOCK) {
1452                                        extractAndWriteExtensionsAsDirectChild(myValue, theEventWriter, def, theResDef, theResource, myChildElem, null, theEncodeContext, theContainedResource);
1453                                } else {
1454                                        String childName = myDef.getChildNameByDatatype(myValue.getClass());
1455                                        encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, myValue, def, childName, false, myParent, false, theEncodeContext);
1456                                        managePrimitiveExtension(myValue, theResDef, theResource, theEventWriter, def, childName, theEncodeContext, theContainedResource);
1457                                }
1458
1459                                theEventWriter.endObject();
1460                        }
1461                }
1462
1463                private void writeUndeclaredExtension(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, IBaseExtension<?, ?> ext, EncodeContext theEncodeContext, boolean theContainedResource) throws IOException {
1464                        IBase value = ext.getValue();
1465                        final String extensionUrl = getExtensionUrl(ext.getUrl());
1466
1467                        theEventWriter.beginObject();
1468
1469                        writeCommentsPreAndPost(myUndeclaredExtension, theEventWriter);
1470
1471                        String elementId = getCompositeElementId(ext);
1472                        if (isNotBlank(elementId)) {
1473                                JsonParser.write(theEventWriter, "id", getCompositeElementId(ext));
1474                        }
1475
1476                        if (isBlank(extensionUrl)) {
1477                                ParseLocation loc = new ParseLocation(theEncodeContext.toString());
1478                                getErrorHandler().missingRequiredElement(loc, "url");
1479                        }
1480
1481                        JsonParser.write(theEventWriter, "url", extensionUrl);
1482
1483                        boolean noValue = value == null || value.isEmpty();
1484                        if (noValue && ext.getExtension().isEmpty()) {
1485
1486                                ParseLocation loc = new ParseLocation(theEncodeContext.toString());
1487                                getErrorHandler().missingRequiredElement(loc, "value");
1488                                ourLog.debug("Extension with URL[{}] has no value", extensionUrl);
1489
1490                        } else {
1491
1492                                if (!noValue && !ext.getExtension().isEmpty()) {
1493                                        ParseLocation loc = new ParseLocation(theEncodeContext.toString());
1494                                        getErrorHandler().extensionContainsValueAndNestedExtensions(loc);
1495                                }
1496
1497                                // Write child extensions
1498                                if (!ext.getExtension().isEmpty()) {
1499
1500                                        if (myModifier) {
1501                                                beginArray(theEventWriter, "modifierExtension");
1502                                        } else {
1503                                                beginArray(theEventWriter, "extension");
1504                                        }
1505
1506                                        for (Object next : ext.getExtension()) {
1507                                                writeUndeclaredExtension(theResDef, theResource, theEventWriter, (IBaseExtension<?, ?>) next, theEncodeContext, theContainedResource);
1508                                        }
1509                                        theEventWriter.endArray();
1510
1511                                }
1512
1513                                // Write value
1514                                if (!noValue) {
1515                                        theEncodeContext.pushPath("value", false);
1516
1517                                        /*
1518                                         * Pre-process value - This is called in case the value is a reference
1519                                         * since we might modify the text
1520                                         */
1521                                        value = preProcessValues(myDef, theResource, Collections.singletonList(value), myChildElem, theEncodeContext).get(0);
1522
1523                                        RuntimeChildUndeclaredExtensionDefinition extDef = getContext().getRuntimeChildUndeclaredExtensionDefinition();
1524                                        String childName = extDef.getChildNameByDatatype(value.getClass());
1525                                        if (childName == null) {
1526                                                childName = "value" + WordUtils.capitalize(getContext().getElementDefinition(value.getClass()).getName());
1527                                        }
1528                                        BaseRuntimeElementDefinition<?> childDef = extDef.getChildElementDefinitionByDatatype(value.getClass());
1529                                        if (childDef == null) {
1530                                                throw new ConfigurationException("Unable to encode extension, unrecognized child element type: " + value.getClass().getCanonicalName());
1531                                        }
1532                                        encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, false, myParent, false, theEncodeContext);
1533                                        managePrimitiveExtension(value, theResDef, theResource, theEventWriter, childDef, childName, theEncodeContext, theContainedResource);
1534
1535                                        theEncodeContext.popPath();
1536                                }
1537                        }
1538
1539                        // theEventWriter.name(myUndeclaredExtension.get);
1540
1541                        theEventWriter.endObject();
1542                }
1543        }
1544
1545        private static void write(JsonLikeWriter theWriter, String theName, String theValue) throws IOException {
1546                theWriter.write(theName, theValue);
1547        }
1548}