package ca.uhn.fhir.parser;

/*
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 - 2017 University Health Network
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.text.WordUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseBooleanDatatype;
import org.hl7.fhir.instance.model.api.IBaseDecimalDatatype;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions;
import org.hl7.fhir.instance.model.api.IBaseIntegerDatatype;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IDomainResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.INarrative;
import org.hl7.fhir.instance.model.api.IPrimitiveType;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeChildContainedResources;
import ca.uhn.fhir.context.RuntimeChildDeclaredExtensionDefinition;
import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition;
import ca.uhn.fhir.context.RuntimeChildUndeclaredExtensionDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.api.BaseBundle;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.BundleEntry;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.Tag;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.model.base.composite.BaseContainedDt;
import ca.uhn.fhir.model.primitive.DecimalDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.narrative.INarrativeGenerator;
import ca.uhn.fhir.parser.json.GsonStructure;
import ca.uhn.fhir.parser.json.JsonLikeArray;
import ca.uhn.fhir.parser.json.JsonLikeObject;
import ca.uhn.fhir.parser.json.JsonLikeStructure;
import ca.uhn.fhir.parser.json.JsonLikeValue;
import ca.uhn.fhir.parser.json.JsonLikeValue.ScalarType;
import ca.uhn.fhir.parser.json.JsonLikeValue.ValueType;
import ca.uhn.fhir.parser.json.JsonLikeWriter;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.util.ElementUtil;

/**
 * This class is the FHIR JSON parser/encoder. Users should not interact with this class directly, but should use
 * {@link FhirContext#newJsonParser()} to get an instance.
 */
public class JsonParser extends BaseParser implements IJsonLikeParser {

	private static final Set<String> BUNDLE_TEXTNODE_CHILDREN_DSTU1;
	private static final Set<String> BUNDLE_TEXTNODE_CHILDREN_DSTU2;
	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonParser.HeldExtension.class);

	static {
		HashSet<String> hashSetDstu1 = new HashSet<String>();
		hashSetDstu1.add("title");
		hashSetDstu1.add("id");
		hashSetDstu1.add("updated");
		hashSetDstu1.add("published");
		hashSetDstu1.add("totalResults");
		BUNDLE_TEXTNODE_CHILDREN_DSTU1 = Collections.unmodifiableSet(hashSetDstu1);

		HashSet<String> hashSetDstu2 = new HashSet<String>();
		hashSetDstu2.add("type");
		hashSetDstu2.add("base");
		hashSetDstu2.add("total");
		BUNDLE_TEXTNODE_CHILDREN_DSTU2 = Collections.unmodifiableSet(hashSetDstu2);
	}

	private FhirContext myContext;
	private boolean myPrettyPrint;

	/**
	 * Do not use this constructor, the recommended way to obtain a new instance of the JSON parser is to invoke
	 * {@link FhirContext#newJsonParser()}.
	 *
	 * @param theParserErrorHandler
	 */
	public JsonParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler) {
		super(theContext, theParserErrorHandler);
		myContext = theContext;
	}

	private boolean addToHeldComments(int valueIdx, List<String> theCommentsToAdd, ArrayList<ArrayList<String>> theListToAddTo) {
		if (theCommentsToAdd.size() > 0) {
			theListToAddTo.ensureCapacity(valueIdx);
			while (theListToAddTo.size() <= valueIdx) {
				theListToAddTo.add(null);
			}
			if (theListToAddTo.get(valueIdx) == null) {
				theListToAddTo.set(valueIdx, new ArrayList<String>());
			}
			theListToAddTo.get(valueIdx).addAll(theCommentsToAdd);
			return true;
		}
		return false;
	}

	private boolean addToHeldExtensions(int valueIdx, List<? extends IBaseExtension<?, ?>> ext, ArrayList<ArrayList<HeldExtension>> list, boolean theIsModifier, CompositeChildElement theChildElem) {
		if (ext.size() > 0) {
			list.ensureCapacity(valueIdx);
			while (list.size() <= valueIdx) {
				list.add(null);
			}
			if (list.get(valueIdx) == null) {
				list.set(valueIdx, new ArrayList<JsonParser.HeldExtension>());
			}
			for (IBaseExtension<?, ?> next : ext) {
				list.get(valueIdx).add(new HeldExtension(next, theIsModifier, theChildElem));
			}
			return true;
		}
		return false;
	}

	private void addToHeldIds(int theValueIdx, ArrayList<String> theListToAddTo, String theId) {
		theListToAddTo.ensureCapacity(theValueIdx);
		while (theListToAddTo.size() <= theValueIdx) {
			theListToAddTo.add(null);
		}
		if (theListToAddTo.get(theValueIdx) == null) {
			theListToAddTo.set(theValueIdx, theId);
		}
	}

//	private void assertObjectOfType(JsonLikeValue theResourceTypeObj, Object theValueType, String thePosition) {
//		if (theResourceTypeObj == null) {
//			throw new DataFormatException("Invalid JSON content detected, missing required element: '" + thePosition + "'");
//		}
//
//		if (theResourceTypeObj.getValueType() != theValueType) {
//			throw new DataFormatException("Invalid content of element " + thePosition + ", expected " + theValueType);
//		}
//	}

	private void beginArray(JsonLikeWriter theEventWriter, String arrayName) throws IOException {
		theEventWriter.beginArray(arrayName);
	}

	private void beginObject(JsonLikeWriter theEventWriter, String arrayName) throws IOException {
		theEventWriter.beginObject(arrayName);
	}

	private JsonLikeWriter createJsonWriter(Writer theWriter) {
		JsonLikeStructure jsonStructure = new GsonStructure();
		JsonLikeWriter retVal = jsonStructure.getJsonLikeWriter(theWriter);
		return retVal;
	}

	@Override
	public void doEncodeBundleToWriter(Bundle theBundle, Writer theWriter) throws IOException {
		JsonLikeWriter eventWriter = createJsonWriter(theWriter);
		doEncodeBundleToJsonLikeWriter(theBundle, eventWriter);
	}
	
	public void doEncodeBundleToJsonLikeWriter(Bundle theBundle, JsonLikeWriter theEventWriter) throws IOException { 
		if (myPrettyPrint) {
			theEventWriter.setPrettyPrint(myPrettyPrint);
		}
		theEventWriter.init();

		if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) {
			encodeBundleToWriterInDstu2Format(theBundle, theEventWriter);
		} else {
			encodeBundleToWriterInDstu1Format(theBundle, theEventWriter);
		}
		theEventWriter.flush();
	}

	@Override
	protected void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException {
		JsonLikeWriter eventWriter = createJsonWriter(theWriter);
		doEncodeResourceToJsonLikeWriter(theResource, eventWriter);
	}
	
	public void doEncodeResourceToJsonLikeWriter(IBaseResource theResource, JsonLikeWriter theEventWriter) throws IOException { 
		if (myPrettyPrint) {
			theEventWriter.setPrettyPrint(myPrettyPrint);
		}
		theEventWriter.init();

		RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);
		encodeResourceToJsonStreamWriter(resDef, theResource, theEventWriter, null, false, false);
		theEventWriter.flush();
	}

	@Override
	public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) {
		JsonLikeStructure jsonStructure = new GsonStructure();
		jsonStructure.load(theReader);
		
		T retVal = doParseResource(theResourceType, jsonStructure);
		
		return retVal;
	}

	public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, JsonLikeStructure theJsonStructure) {
			JsonLikeObject object = theJsonStructure.getRootObject();
		
			JsonLikeValue resourceTypeObj = object.get("resourceType");
			if (resourceTypeObj == null || !resourceTypeObj.isString() || isBlank(resourceTypeObj.getAsString())) {
				throw new DataFormatException("Invalid JSON content detected, missing required element: 'resourceType'");
			}
			
			String resourceType = resourceTypeObj.getAsString();

			ParserState<? extends IBaseResource> state = ParserState.getPreResourceInstance(this, theResourceType, myContext, true, getErrorHandler());
			state.enteringNewElement(null, resourceType);

			parseChildren(object, state);

			state.endingElement();
			state.endingElement();

			@SuppressWarnings("unchecked")
			T retVal = (T) state.getObject();

			return retVal;
	}

	@Override
	public void encodeBundleToJsonLikeWriter(Bundle theBundle, JsonLikeWriter theJsonLikeWriter) throws IOException, DataFormatException {
		Validate.notNull(theBundle, "theBundle must not be null");
		Validate.notNull(theJsonLikeWriter, "theJsonLikeWriter must not be null");

		doEncodeBundleToJsonLikeWriter(theBundle, theJsonLikeWriter);
	}

	private void encodeBundleToWriterInDstu1Format(Bundle theBundle, JsonLikeWriter theEventWriter) throws IOException {
		theEventWriter.beginObject();

		write(theEventWriter, "resourceType", "Bundle");

		writeTagWithTextNode(theEventWriter, "title", theBundle.getTitle());
		writeTagWithTextNode(theEventWriter, "id", theBundle.getBundleId());
		writeOptionalTagWithTextNode(theEventWriter, "updated", theBundle.getUpdated());

		boolean linkStarted = false;
		linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "self", theBundle.getLinkSelf(), linkStarted);
		linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "first", theBundle.getLinkFirst(), linkStarted);
		linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "previous", theBundle.getLinkPrevious(), linkStarted);
		linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "next", theBundle.getLinkNext(), linkStarted);
		linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "last", theBundle.getLinkLast(), linkStarted);
		linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "fhir-base", theBundle.getLinkBase(), linkStarted);
		if (linkStarted) {
			theEventWriter.endArray();
		}

		writeCategories(theEventWriter, theBundle.getCategories());

		writeOptionalTagWithTextNode(theEventWriter, "totalResults", theBundle.getTotalResults());

		writeAuthor(theBundle, theEventWriter);

		beginArray(theEventWriter, "entry");
		for (BundleEntry nextEntry : theBundle.getEntries()) {
			theEventWriter.beginObject();

			boolean deleted = nextEntry.getDeletedAt() != null && nextEntry.getDeletedAt().isEmpty() == false;
			if (deleted) {
				writeTagWithTextNode(theEventWriter, "deleted", nextEntry.getDeletedAt());
			}
			writeTagWithTextNode(theEventWriter, "title", nextEntry.getTitle());
			//TODO: Use of a deprecated method should be resolved.
			writeTagWithTextNode(theEventWriter, "id", nextEntry.getId());

			linkStarted = false;
			linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "self", nextEntry.getLinkSelf(), linkStarted);
			linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "alternate", nextEntry.getLinkAlternate(), linkStarted);
			linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "search", nextEntry.getLinkSearch(), linkStarted);
			if (linkStarted) {
				theEventWriter.endArray();
			}

			//TODO: Use of a deprecated method should be resolved.
			writeOptionalTagWithTextNode(theEventWriter, "updated", nextEntry.getUpdated());
			writeOptionalTagWithTextNode(theEventWriter, "published", nextEntry.getPublished());

			writeCategories(theEventWriter, nextEntry.getCategories());

			writeAuthor(nextEntry, theEventWriter);

			IResource resource = nextEntry.getResource();
			if (resource != null && !resource.isEmpty() && !deleted) {
				RuntimeResourceDefinition resDef = myContext.getResourceDefinition(resource);
				encodeResourceToJsonStreamWriter(resDef, resource, theEventWriter, "content", false, true);
			}

			if (nextEntry.getSummary().isEmpty() == false) {
				write(theEventWriter, "summary", nextEntry.getSummary().getValueAsString());
			}

			theEventWriter.endObject(); // entry object
		}
		theEventWriter.endArray(); // entry array

		theEventWriter.endObject(); // resource object
	}

	private void encodeBundleToWriterInDstu2Format(Bundle theBundle, JsonLikeWriter theEventWriter) throws IOException {
		theEventWriter.beginObject();

		write(theEventWriter, "resourceType", "Bundle");

		writeOptionalTagWithTextNode(theEventWriter, "id", theBundle.getId().getIdPart());

		if (!ElementUtil.isEmpty(theBundle.getId().getVersionIdPart(), theBundle.getUpdated())) {
			beginObject(theEventWriter, "meta");
			writeOptionalTagWithTextNode(theEventWriter, "versionId", theBundle.getId().getVersionIdPart());
			writeOptionalTagWithTextNode(theEventWriter, "lastUpdated", theBundle.getUpdated());
			theEventWriter.endObject();
		}

		writeOptionalTagWithTextNode(theEventWriter, "type", theBundle.getType());

		writeOptionalTagWithNumberNode(theEventWriter, "total", theBundle.getTotalResults());

		boolean linkStarted = false;
		linkStarted = writeAtomLinkInDstu2Format(theEventWriter, "next", theBundle.getLinkNext(), linkStarted);
		linkStarted = writeAtomLinkInDstu2Format(theEventWriter, "self", theBundle.getLinkSelf(), linkStarted);
		linkStarted = writeAtomLinkInDstu2Format(theEventWriter, "first", theBundle.getLinkFirst(), linkStarted);
		linkStarted = writeAtomLinkInDstu2Format(theEventWriter, "previous", theBundle.getLinkPrevious(), linkStarted);
		linkStarted = writeAtomLinkInDstu2Format(theEventWriter, "last", theBundle.getLinkLast(), linkStarted);
		if (linkStarted) {
			theEventWriter.endArray();
		}

		beginArray(theEventWriter, "entry");
		for (BundleEntry nextEntry : theBundle.getEntries()) {
			theEventWriter.beginObject();

			if (nextEntry.getResource() != null && isNotBlank(nextEntry.getResource().getIdElement().getValue()) && (nextEntry.getResource().getId().getBaseUrl() != null || nextEntry.getResource().getId().getValueAsString().startsWith("urn:"))) {
				writeOptionalTagWithTextNode(theEventWriter, "fullUrl", nextEntry.getResource().getId().getValue());
			}

			boolean deleted = nextEntry.getDeletedAt() != null && nextEntry.getDeletedAt().isEmpty() == false;
			IResource resource = nextEntry.getResource();
			if (resource != null && !resource.isEmpty() && !deleted) {
				RuntimeResourceDefinition resDef = myContext.getResourceDefinition(resource);
				encodeResourceToJsonStreamWriter(resDef, resource, theEventWriter, "resource", false, true);
			}

			if (nextEntry.getSearchMode().isEmpty() == false || nextEntry.getScore().isEmpty() == false) {
				beginObject(theEventWriter, "search");
				writeOptionalTagWithTextNode(theEventWriter, "mode", nextEntry.getSearchMode().getValueAsString());
				writeOptionalTagWithDecimalNode(theEventWriter, "score", nextEntry.getScore());
				theEventWriter.endObject();
				// IResource nextResource = nextEntry.getResource();
			}

			if (nextEntry.getTransactionMethod().isEmpty() == false || nextEntry.getLinkSearch().isEmpty() == false) {
				beginObject(theEventWriter, "request");
				writeOptionalTagWithTextNode(theEventWriter, "method", nextEntry.getTransactionMethod().getValue());
				writeOptionalTagWithTextNode(theEventWriter, "url", nextEntry.getLinkSearch().getValue());
				theEventWriter.endObject();
			}

			if (deleted) {
				beginObject(theEventWriter, "deleted");
				if (nextEntry.getResource() != null) {
					write(theEventWriter, "type", myContext.getResourceDefinition(nextEntry.getResource()).getName());
					writeOptionalTagWithTextNode(theEventWriter, "resourceId", nextEntry.getResource().getId().getIdPart());
					writeOptionalTagWithTextNode(theEventWriter, "versionId", nextEntry.getResource().getId().getVersionIdPart());
				}
				writeTagWithTextNode(theEventWriter, "instant", nextEntry.getDeletedAt());
				theEventWriter.endObject();
			}

			// linkStarted = false;
			// linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "self", nextEntry.getLinkSelf(), linkStarted);
			// linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "alternate", nextEntry.getLinkAlternate(),
			// linkStarted);
			// linkStarted = writeAtomLinkInDstu1Format(theEventWriter, "search", nextEntry.getLinkSearch(),
			// linkStarted);
			// if (linkStarted) {
			// theEventWriter.writeEnd();
			// }
			//
			// writeOptionalTagWithTextNode(theEventWriter, "updated", nextEntry.getUpdated());
			// writeOptionalTagWithTextNode(theEventWriter, "published", nextEntry.getPublished());
			//
			// writeCategories(theEventWriter, nextEntry.getCategories());
			//
			// writeAuthor(nextEntry, theEventWriter);

			if (nextEntry.getSummary().isEmpty() == false) {
				write(theEventWriter, "summary", nextEntry.getSummary().getValueAsString());
			}

			theEventWriter.endObject(); // entry object
		}
		theEventWriter.endArray(); // entry array

		theEventWriter.endObject(); // resource object
	}

	private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, IBase theNextValue, BaseRuntimeElementDefinition<?> theChildDef, String theChildName, boolean theContainedResource, CompositeChildElement theChildElem,
			boolean theForceEmpty) throws IOException {

		switch (theChildDef.getChildType()) {
		case ID_DATATYPE: {
			IIdType value = (IIdType) theNextValue;
			String encodedValue = "id".equals(theChildName) ? value.getIdPart() : value.getValue();
			if (isBlank(encodedValue)) {
				break;
			}
			if (theChildName != null) {
				write(theEventWriter, theChildName, encodedValue);
			} else {
				theEventWriter.write(encodedValue);
			}
			break;
		}
		case PRIMITIVE_DATATYPE: {
			final IPrimitiveType<?> value = (IPrimitiveType<?>) theNextValue;
			if (isBlank(value.getValueAsString())) {
				if (theForceEmpty) {
					theEventWriter.writeNull();
				}
				break;
			}

			if (value instanceof IBaseIntegerDatatype) {
				if (theChildName != null) {
					write(theEventWriter, theChildName, ((IBaseIntegerDatatype) value).getValue());
				} else {
					theEventWriter.write(((IBaseIntegerDatatype) value).getValue());
				}
			} else if (value instanceof IBaseDecimalDatatype) {
				BigDecimal decimalValue = ((IBaseDecimalDatatype) value).getValue();
				decimalValue = new BigDecimal(decimalValue.toString()) {
					private static final long serialVersionUID = 1L;

					@Override
					public String toString() {
						return value.getValueAsString();
					}
				};
				if (theChildName != null) {
					write(theEventWriter, theChildName, decimalValue);
				} else {
					theEventWriter.write(decimalValue);
				}
			} else if (value instanceof IBaseBooleanDatatype) {
				if (theChildName != null) {
					write(theEventWriter, theChildName, ((IBaseBooleanDatatype) value).getValue());
				} else {
					Boolean booleanValue = ((IBaseBooleanDatatype) value).getValue();
					if (booleanValue != null) {
						theEventWriter.write(booleanValue.booleanValue());
					}
				}
			} else {
				String valueStr = value.getValueAsString();
				if (theChildName != null) {
					write(theEventWriter, theChildName, valueStr);
				} else {
					theEventWriter.write(valueStr);
				}
			}
			break;
		}
		case RESOURCE_BLOCK:
		case COMPOSITE_DATATYPE: {
			if (theChildName != null) {
				theEventWriter.beginObject(theChildName);
			} else {
				theEventWriter.beginObject();
			}
			encodeCompositeElementToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theChildElem);
			theEventWriter.endObject();
			break;
		}
		case CONTAINED_RESOURCE_LIST:
		case CONTAINED_RESOURCES: {
			/*
			 * Disabled per #103 ContainedDt value = (ContainedDt) theNextValue; for (IResource next :
			 * value.getContainedResources()) { if (getContainedResources().getResourceId(next) != null) { continue; }
			 * encodeResourceToJsonStreamWriter(theResDef, next, theWriter, null, true,
			 * fixContainedResourceId(next.getId().getValue())); }
			 */
			List<IBaseResource> containedResources = getContainedResources().getContainedResources();
			if (containedResources.size() > 0) {
				beginArray(theEventWriter, theChildName);

				for (IBaseResource next : containedResources) {
					IIdType resourceId = getContainedResources().getResourceId(next);
					encodeResourceToJsonStreamWriter(theResDef, next, theEventWriter, null, true, fixContainedResourceId(resourceId.getValue()));
				}

				theEventWriter.endArray();
			}
			break;
		}
		case PRIMITIVE_XHTML_HL7ORG:
		case PRIMITIVE_XHTML: {
			if (!isSuppressNarratives()) {
				IPrimitiveType<?> dt = (IPrimitiveType<?>) theNextValue;
				if (theChildName != null) {
					write(theEventWriter, theChildName, dt.getValueAsString());
				} else {
					theEventWriter.write(dt.getValueAsString());
				}
			} else {
				if (theChildName != null) {
					// do nothing
				} else {
					theEventWriter.writeNull();
				}
			}
			break;
		}
		case RESOURCE:
			IBaseResource resource = (IBaseResource) theNextValue;
			RuntimeResourceDefinition def = myContext.getResourceDefinition(resource);
			encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, false, true);
			break;
		case UNDECL_EXT:
		default:
			throw new IllegalStateException("Should not have this state here: " + theChildDef.getChildType().name());
		}

	}

	private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theElement, JsonLikeWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent) throws IOException {

		{
			String elementId = getCompositeElementId(theElement);
			if (isNotBlank(elementId)) {
				write(theEventWriter, "id", elementId);
			}
		}

		boolean haveWrittenExtensions = false;
		for (CompositeChildElement nextChildElem : super.compositeChildIterator(theElement, theContainedResource, theParent)) {

			BaseRuntimeChildDefinition nextChild = nextChildElem.getDef();

			if (nextChildElem.getDef().getElementName().equals("extension") || nextChildElem.getDef().getElementName().equals("modifierExtension") || nextChild instanceof RuntimeChildDeclaredExtensionDefinition) {
				if (!haveWrittenExtensions) {
					extractAndWriteExtensionsAsDirectChild(theElement, theEventWriter, myContext.getElementDefinition(theElement.getClass()), theResDef, theResource, nextChildElem);
					haveWrittenExtensions = true;
				}
				continue;
			}

			if (nextChild instanceof RuntimeChildNarrativeDefinition) {
				INarrativeGenerator gen = myContext.getNarrativeGenerator();
				if (gen != null) {
					INarrative narr;
					if (theResource instanceof IResource) {
						narr = ((IResource) theResource).getText();
					} else if (theResource instanceof IDomainResource) {
						narr = ((IDomainResource) theResource).getText();
					} else {
						narr = null;
					}
					if (narr != null && narr.isEmpty()) {
						gen.generateNarrative(myContext, theResource, narr);
						if (!narr.isEmpty()) {
							RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
							String childName = nextChild.getChildNameByDatatype(child.getDatatype());
							BaseRuntimeElementDefinition<?> type = child.getChildByName(childName);
							encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, narr, type, childName, theContainedResource, nextChildElem, false);
							continue;
						}
					}
				}
			} else if (nextChild instanceof RuntimeChildContainedResources) {
				String childName = nextChild.getValidChildNames().iterator().next();
				BaseRuntimeElementDefinition<?> child = nextChild.getChildByName(childName);
				encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, null, child, childName, theContainedResource, nextChildElem, false);
				continue;
			}

			List<? extends IBase> values = nextChild.getAccessor().getValues(theElement);
			values = super.preProcessValues(nextChild, theResource, values, nextChildElem);

			if (values == null || values.isEmpty()) {
				continue;
			}

			String currentChildName = null;
			boolean inArray = false;

			ArrayList<ArrayList<HeldExtension>> extensions = new ArrayList<ArrayList<HeldExtension>>(0);
			ArrayList<ArrayList<HeldExtension>> modifierExtensions = new ArrayList<ArrayList<HeldExtension>>(0);
			ArrayList<ArrayList<String>> comments = new ArrayList<ArrayList<String>>(0);
			ArrayList<String> ids = new ArrayList<String>(0);

			int valueIdx = 0;
			for (IBase nextValue : values) {

				if (nextValue == null || nextValue.isEmpty()) {
					if (nextValue instanceof BaseContainedDt) {
						if (theContainedResource || getContainedResources().isEmpty()) {
							continue;
						}
					} else {
						continue;
					}
				}

				BaseParser.ChildNameAndDef childNameAndDef = super.getChildNameAndDef(nextChild, nextValue);
				if (childNameAndDef == null) {
					continue;
				}
				
				String childName = childNameAndDef.getChildName();
				BaseRuntimeElementDefinition<?> childDef = childNameAndDef.getChildDef();
				boolean primitive = childDef.getChildType() == ChildTypeEnum.PRIMITIVE_DATATYPE;

				if ((childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) && theContainedResource) {
					continue;
				}

				boolean force = false;
				if (primitive) {
					if (nextValue instanceof ISupportsUndeclaredExtensions) {
						List<ExtensionDt> ext = ((ISupportsUndeclaredExtensions) nextValue).getUndeclaredExtensions();
						force |= addToHeldExtensions(valueIdx, ext, extensions, false, nextChildElem);

						ext = ((ISupportsUndeclaredExtensions) nextValue).getUndeclaredModifierExtensions();
						force |= addToHeldExtensions(valueIdx, ext, modifierExtensions, true, nextChildElem);
					} else {
						if (nextValue instanceof IBaseHasExtensions) {
							IBaseHasExtensions element = (IBaseHasExtensions) nextValue;
							List<? extends IBaseExtension<?, ?>> ext = element.getExtension();
							force |= addToHeldExtensions(valueIdx, ext, extensions, false, nextChildElem);
						}
						if (nextValue instanceof IBaseHasModifierExtensions) {
							IBaseHasModifierExtensions element = (IBaseHasModifierExtensions) nextValue;
							List<? extends IBaseExtension<?, ?>> ext = element.getModifierExtension();
							force |= addToHeldExtensions(valueIdx, ext, extensions, true, nextChildElem);
						}
					}
					if (nextValue.hasFormatComment()) {
						force |= addToHeldComments(valueIdx, nextValue.getFormatCommentsPre(), comments);
						force |= addToHeldComments(valueIdx, nextValue.getFormatCommentsPost(), comments);
					}
					String elementId = getCompositeElementId(nextValue);
					if (isNotBlank(elementId)) {
						force = true;
						addToHeldIds(valueIdx, ids, elementId);
					}
				}

				if (currentChildName == null || !currentChildName.equals(childName)) {
					if (inArray) {
						theEventWriter.endArray();
					}
					if (nextChild.getMax() > 1 || nextChild.getMax() == Child.MAX_UNLIMITED) {
						beginArray(theEventWriter, childName);
						inArray = true;
						encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, nextChildElem, force);
					} else if (nextChild instanceof RuntimeChildNarrativeDefinition && theContainedResource) {
						// suppress narratives from contained resources
					} else {
						encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, childName, theContainedResource, nextChildElem, false);
					}
					currentChildName = childName;
				} else {
					encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, nextValue, childDef, null, theContainedResource, nextChildElem, force);
				}

				valueIdx++;
			}

			if (inArray) {
				theEventWriter.endArray();
			}

			if (!extensions.isEmpty() || !modifierExtensions.isEmpty() || !comments.isEmpty()) {
				if (inArray) {
					// If this is a repeatable field, the extensions go in an array too
					beginArray(theEventWriter, '_' + currentChildName);
				} else {
					beginObject(theEventWriter, '_' + currentChildName);
				}

				for (int i = 0; i < valueIdx; i++) {
					boolean haveContent = false;

					List<HeldExtension> heldExts = Collections.emptyList();
					List<HeldExtension> heldModExts = Collections.emptyList();
					if (extensions.size() > i && extensions.get(i) != null && extensions.get(i).isEmpty() == false) {
						haveContent = true;
						heldExts = extensions.get(i);
					}

					if (modifierExtensions.size() > i && modifierExtensions.get(i) != null && modifierExtensions.get(i).isEmpty() == false) {
						haveContent = true;
						heldModExts = modifierExtensions.get(i);
					}

					ArrayList<String> nextComments;
					if (comments.size() > i) {
						nextComments = comments.get(i);
					} else {
						nextComments = null;
					}
					if (nextComments != null && nextComments.isEmpty() == false) {
						haveContent = true;
					}

					String elementId = null;
					if (ids.size() > i) {
						elementId = ids.get(i);
						haveContent |= isNotBlank(elementId);
					}

					if (!haveContent) {
						theEventWriter.writeNull();
					} else {
						if (inArray) {
							theEventWriter.beginObject();
						}
						if (isNotBlank(elementId)) {
							write(theEventWriter, "id", elementId);
						}
						if (nextComments != null && !nextComments.isEmpty()) {
							beginArray(theEventWriter, "fhir_comments");
							for (String next : nextComments) {
								theEventWriter.write(next);
							}
							theEventWriter.endArray();
						}
						writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, heldExts, heldModExts);
						if (inArray) {
							theEventWriter.endObject();
						}
					}
				}

				if (inArray) {
					theEventWriter.endArray();
				} else {
					theEventWriter.endObject();
				}
			}
		}
	}

	private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonLikeWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent) throws IOException, DataFormatException {

		writeCommentsPreAndPost(theNextValue, theEventWriter);
		encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theParent);
	}

	@Override
	public void encodeResourceToJsonLikeWriter(IBaseResource theResource, JsonLikeWriter theJsonLikeWriter) throws IOException, DataFormatException {
		Validate.notNull(theResource, "theResource can not be null");
		Validate.notNull(theJsonLikeWriter, "theJsonLikeWriter can not be null");

		if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) {
			throw new IllegalArgumentException("This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum());
		}

		doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter);
	}

	private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull, boolean theContainedResource, boolean theSubResource) throws IOException {
		IIdType resourceId = null;
		//		if (theResource instanceof IResource) {
		//			IResource res = (IResource) theResource;
		//			if (StringUtils.isNotBlank(res.getId().getIdPart())) {
		//				if (theContainedResource) {
		//					resourceId = res.getId().getIdPart();
		//				} else if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) {
		//					resourceId = res.getId().getIdPart();
		//				}
		//			}
		//		} else if (theResource instanceof IAnyResource) {
		//			IAnyResource res = (IAnyResource) theResource;
		//			if (/* theContainedResource && */StringUtils.isNotBlank(res.getIdElement().getIdPart())) {
		//				resourceId = res.getIdElement().getIdPart();
		//			}
		//		}

		if (StringUtils.isNotBlank(theResource.getIdElement().getIdPart())) {
			resourceId = theResource.getIdElement();
			if (theResource.getIdElement().getValue().startsWith("urn:")) {
				resourceId = null;
			}
			if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU1)) {
				resourceId = null;
			}
		}

		if (!theContainedResource) {
			if (super.shouldEncodeResourceId(theResource) == false) {
				resourceId = null;
			} else if (!theSubResource && getEncodeForceResourceId() != null) {
				resourceId = getEncodeForceResourceId();
			}
		}

		encodeResourceToJsonStreamWriter(theResDef, theResource, theEventWriter, theObjectNameOrNull, theContainedResource, resourceId);
	}

	private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, String theObjectNameOrNull, boolean theContainedResource, IIdType theResourceId) throws IOException {
		if (!theContainedResource) {
			super.containResourcesForEncoding(theResource);
		}

		RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);

		if (theObjectNameOrNull == null) {
			theEventWriter.beginObject();
		} else {
			beginObject(theEventWriter, theObjectNameOrNull);
		}

		write(theEventWriter, "resourceType", resDef.getName());
		if (theResourceId != null && theResourceId.hasIdPart()) {
			write(theEventWriter, "id", theResourceId.getIdPart());
			if (theResourceId.hasFormatComment()) {
				beginObject(theEventWriter, "_id");
				writeCommentsPreAndPost(theResourceId, theEventWriter);
				theEventWriter.endObject();
			}
		}

		if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1) && theResource instanceof IResource) {
			IResource resource = (IResource) theResource;
			// Object securityLabelRawObj =

			List<BaseCodingDt> securityLabels = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.SECURITY_LABELS);
			List<? extends IIdType> profiles = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.PROFILES);
			profiles = super.getProfileTagsForEncoding(resource, profiles);

			TagList tags = getMetaTagsForEncoding(resource);
			InstantDt updated = (InstantDt) resource.getResourceMetadata().get(ResourceMetadataKeyEnum.UPDATED);
			IdDt resourceId = resource.getId();
			String versionIdPart = resourceId.getVersionIdPart();
			if (isBlank(versionIdPart)) {
				versionIdPart = ResourceMetadataKeyEnum.VERSION.get(resource);
			}

			if (super.shouldEncodeResourceMeta(resource) && ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles) == false) {
				beginObject(theEventWriter, "meta");
				writeOptionalTagWithTextNode(theEventWriter, "versionId", versionIdPart);
				writeOptionalTagWithTextNode(theEventWriter, "lastUpdated", updated);

				if (profiles != null && profiles.isEmpty() == false) {
					beginArray(theEventWriter, "profile");
					for (IIdType profile : profiles) {
						if (profile != null && isNotBlank(profile.getValue())) {
							theEventWriter.write(profile.getValue());
						}
					}
					theEventWriter.endArray();
				}

				if (securityLabels.isEmpty() == false) {
					beginArray(theEventWriter, "security");
					for (BaseCodingDt securityLabel : securityLabels) {
						theEventWriter.beginObject();
						encodeCompositeElementChildrenToStreamWriter(resDef, resource, securityLabel, theEventWriter, theContainedResource, null);
						theEventWriter.endObject();
					}
					theEventWriter.endArray();
				}

				if (tags != null && tags.isEmpty() == false) {
					beginArray(theEventWriter, "tag");
					for (Tag tag : tags) {
						if (tag.isEmpty()) {
							continue;
						}
						theEventWriter.beginObject();
						writeOptionalTagWithTextNode(theEventWriter, "system", tag.getScheme());
						writeOptionalTagWithTextNode(theEventWriter, "code", tag.getTerm());
						writeOptionalTagWithTextNode(theEventWriter, "display", tag.getLabel());
						theEventWriter.endObject();
					}
					theEventWriter.endArray();
				}

				theEventWriter.endObject(); // end meta
			}
		}

		if (theResource instanceof IBaseBinary) {
			IBaseBinary bin = (IBaseBinary) theResource;
			String contentType = bin.getContentType();
			if (isNotBlank(contentType)) {
				write(theEventWriter, "contentType", contentType);
			}
			String contentAsBase64 = bin.getContentAsBase64();
			if (isNotBlank(contentAsBase64)) {
				write(theEventWriter, "content", contentAsBase64);
			}
		} else {
			encodeCompositeElementToStreamWriter(theResDef, theResource, theResource, theEventWriter, theContainedResource, new CompositeChildElement(resDef));
		}

		theEventWriter.endObject();
	}

	@Override
	public void encodeTagListToWriter(TagList theTagList, Writer theWriter) throws IOException {
		JsonLikeWriter theEventWriter = createJsonWriter(theWriter);
		encodeTagListToJsonLikeWriter(theTagList, theEventWriter);
	}

	@Override
	public void encodeTagListToJsonLikeWriter(TagList theTagList, JsonLikeWriter theEventWriter) throws IOException {
		if (myPrettyPrint) {
			theEventWriter.setPrettyPrint(myPrettyPrint);
		}
		theEventWriter.init();

		theEventWriter.beginObject();

		write(theEventWriter, "resourceType", TagList.ELEMENT_NAME);

		beginArray(theEventWriter, TagList.ATTR_CATEGORY);
		for (Tag next : theTagList) {
			theEventWriter.beginObject();

			if (isNotBlank(next.getTerm())) {
				write(theEventWriter, Tag.ATTR_TERM, next.getTerm());
			}
			if (isNotBlank(next.getLabel())) {
				write(theEventWriter, Tag.ATTR_LABEL, next.getLabel());
			}
			if (isNotBlank(next.getScheme())) {
				write(theEventWriter, Tag.ATTR_SCHEME, next.getScheme());
			}

			theEventWriter.endObject();
		}
		theEventWriter.endArray();

		theEventWriter.endObject();
		theEventWriter.flush();
	}

	/**
	 * This is useful only for the two cases where extensions are encoded as direct children (e.g. not in some object
	 * called _name): resource extensions, and extension extensions
	 * @param theChildElem 
	 */
	private void extractAndWriteExtensionsAsDirectChild(IBase theElement, JsonLikeWriter theEventWriter, BaseRuntimeElementDefinition<?> theElementDef, RuntimeResourceDefinition theResDef, IBaseResource theResource, CompositeChildElement theChildElem) throws IOException {
		List<HeldExtension> extensions = new ArrayList<HeldExtension>(0);
		List<HeldExtension> modifierExtensions = new ArrayList<HeldExtension>(0);

		// Undeclared extensions
		extractUndeclaredExtensions(theElement, extensions, modifierExtensions, theChildElem);

		// Declared extensions
		if (theElementDef != null) {
			extractDeclaredExtensions(theElement, theElementDef, extensions, modifierExtensions, theChildElem);
		}

		// Write the extensions
		writeExtensionsAsDirectChild(theResource, theEventWriter, theResDef, extensions, modifierExtensions);
	}

	private void extractDeclaredExtensions(IBase theResource, BaseRuntimeElementDefinition<?> resDef, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions, CompositeChildElement theChildElem) {
		for (RuntimeChildDeclaredExtensionDefinition nextDef : resDef.getExtensionsNonModifier()) {
			for (IBase nextValue : nextDef.getAccessor().getValues(theResource)) {
				if (nextValue != null) {
					if (nextValue.isEmpty()) {
						continue;
					}
					extensions.add(new HeldExtension(nextDef, nextValue, theChildElem));
				}
			}
		}
		for (RuntimeChildDeclaredExtensionDefinition nextDef : resDef.getExtensionsModifier()) {
			for (IBase nextValue : nextDef.getAccessor().getValues(theResource)) {
				if (nextValue != null) {
					if (nextValue.isEmpty()) {
						continue;
					}
					modifierExtensions.add(new HeldExtension(nextDef, nextValue, theChildElem));
				}
			}
		}
	}

	private void extractUndeclaredExtensions(IBase theElement, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions, CompositeChildElement theChildElem) {
		if (theElement instanceof ISupportsUndeclaredExtensions) {
			ISupportsUndeclaredExtensions element = (ISupportsUndeclaredExtensions) theElement;
			List<ExtensionDt> ext = element.getUndeclaredExtensions();
			for (ExtensionDt next : ext) {
				if (next == null || next.isEmpty()) {
					continue;
				}
				extensions.add(new HeldExtension(next, false, theChildElem));
			}

			ext = element.getUndeclaredModifierExtensions();
			for (ExtensionDt next : ext) {
				if (next == null || next.isEmpty()) {
					continue;
				}
				modifierExtensions.add(new HeldExtension(next, true, theChildElem));
			}
		} else {
			if (theElement instanceof IBaseHasExtensions) {
				IBaseHasExtensions element = (IBaseHasExtensions) theElement;
				List<? extends IBaseExtension<?, ?>> ext = element.getExtension();
				for (IBaseExtension<?, ?> next : ext) {
					if (next == null || (ElementUtil.isEmpty(next.getValue()) && next.getExtension().isEmpty())) {
						continue;
					}
					extensions.add(new HeldExtension(next, false, theChildElem));
				}
			}
			if (theElement instanceof IBaseHasModifierExtensions) {
				IBaseHasModifierExtensions element = (IBaseHasModifierExtensions) theElement;
				List<? extends IBaseExtension<?, ?>> ext = element.getModifierExtension();
				for (IBaseExtension<?, ?> next : ext) {
					if (next == null || next.isEmpty()) {
						continue;
					}
					modifierExtensions.add(new HeldExtension(next, true, theChildElem));
				}
			}
		}
	}

	@Override
	public EncodingEnum getEncoding() {
		return EncodingEnum.JSON;
	}

	private JsonLikeArray grabJsonArray(JsonLikeObject theObject, String nextName, String thePosition) {
		JsonLikeValue object = theObject.get(nextName);
		if (object == null || object.isNull()) {
			return null;
		}
		if (!object.isArray()) {
			throw new DataFormatException("Syntax error parsing JSON FHIR structure: Expected ARRAY at element '" + thePosition + "', found '" + object.getJsonType() + "'");
		}
		return object.getAsArray();
	}

//	private JsonObject parse(Reader theReader) {
//
//		PushbackReader pbr = new PushbackReader(theReader);
//		JsonObject object;
//		try {
//			while(true) {
//				int nextInt;
//					nextInt = pbr.read();
//				if (nextInt == -1) {
//					throw new DataFormatException("Did not find any content to parse");
//				}
//				if (nextInt == '{') {
//					pbr.unread('{');
//					break;
//				}
//				if (Character.isWhitespace(nextInt)) {
//					continue;
//				}
//				throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{')");
//			}
//		
//			Gson gson = newGson();
//		
//			object = gson.fromJson(pbr, JsonObject.class);
//		} catch (Exception e) {
//			throw new DataFormatException("Failed to parse JSON content, error was: " + e.getMessage(), e);
//		}
//		
//		return object;
//	}

	private void parseAlternates(JsonLikeValue theAlternateVal, ParserState<?> theState, String theElementName, String theAlternateName) {
		if (theAlternateVal == null || theAlternateVal.isNull()) {
			return;
		}

		if (theAlternateVal.isArray()) {
			JsonLikeArray array = theAlternateVal.getAsArray();
			if (array.size() > 1) {
				throw new DataFormatException("Unexpected array of length " + array.size() + " (expected 0 or 1) for element: " + theElementName);
			}
			if (array.size() == 0) {
				return;
			}
			parseAlternates(array.get(0), theState, theElementName, theAlternateName);
			return;
		}

		JsonLikeValue alternateVal = theAlternateVal;
		if (alternateVal.isObject() == false) {
			getErrorHandler().incorrectJsonType(null, theAlternateName, ValueType.OBJECT, null, alternateVal.getJsonType(), null);
			return;
		}

		JsonLikeObject alternate = alternateVal.getAsObject();
		for (String nextKey : alternate.keySet()) {
			JsonLikeValue nextVal = alternate.get(nextKey);
			if ("extension".equals(nextKey)) {
				boolean isModifier = false;
				JsonLikeArray array = nextVal.getAsArray();
				parseExtension(theState, array, isModifier);
			} else if ("modifierExtension".equals(nextKey)) {
				boolean isModifier = true;
				JsonLikeArray array = nextVal.getAsArray();
				parseExtension(theState, array, isModifier);
			} else if ("id".equals(nextKey)) {
				if (nextVal.isString()) {
					theState.attributeValue("id", nextVal.getAsString());
				} else {
					getErrorHandler().incorrectJsonType(null, "id", ValueType.SCALAR, ScalarType.STRING, nextVal.getJsonType(), nextVal.getDataType());
				}
			} else if ("fhir_comments".equals(nextKey)) {
				parseFhirComments(nextVal, theState);
			}
		}
	}

	@Override
	public <T extends IBaseResource> Bundle parseBundle(Class<T> theResourceType, Reader theReader) {
		JsonLikeStructure jsonStructure = new GsonStructure();
		jsonStructure.load(theReader);
		
		Bundle retVal = parseBundle(theResourceType, jsonStructure);
		
		return retVal;
	}

	@Override
	public Bundle parseBundle(JsonLikeStructure theJsonLikeStructure) throws DataFormatException {
		return parseBundle(null, theJsonLikeStructure);
	}

	@Override
	public <T extends IBaseResource> Bundle parseBundle(Class<T> theResourceType, JsonLikeStructure theJsonStructure) {
		JsonLikeObject object = theJsonStructure.getRootObject();
		
		JsonLikeValue resourceTypeObj = object.get("resourceType");
		if (resourceTypeObj == null || !resourceTypeObj.isString()) {
			throw new DataFormatException("Invalid JSON content detected, missing required element: 'resourceType'");
		}
		String resourceType = resourceTypeObj.getAsString();
		if (!"Bundle".equals(resourceType)) {
			throw new DataFormatException("Trying to parse bundle but found resourceType other than 'Bundle'. Found: '" + resourceType + "'");
		}

		ParserState<Bundle> state = ParserState.getPreAtomInstance(this, myContext, theResourceType, true, getErrorHandler());
		if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) {
			state.enteringNewElement(null, "Bundle");
		} else {
			state.enteringNewElement(null, "feed");
		}

		parseBundleChildren(object, state);

		state.endingElement();
		state.endingElement();

		Bundle retVal = state.getObject();

		return retVal;
	}

	private void parseBundleChildren(JsonLikeObject theObject, ParserState<?> theState) {
		for (String nextName : theObject.keySet()) {
			if ("resourceType".equals(nextName)) {
				continue;
			} else if ("entry".equals(nextName)) {
				JsonLikeArray entries = grabJsonArray(theObject, nextName, "entry");
				for (int i = 0; i < entries.size(); i++) {
					JsonLikeValue jsonValue = entries.get(i);
					theState.enteringNewElement(null, "entry");
					parseBundleChildren(jsonValue.getAsObject(), theState);
					theState.endingElement();
				}
				continue;
			} else if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) {
				if ("link".equals(nextName)) {
					JsonLikeArray entries = grabJsonArray(theObject, nextName, "link");
					for (int i = 0; i < entries.size(); i++) {
						JsonLikeValue jsonValue = entries.get(i);
						theState.enteringNewElement(null, "link");
						JsonLikeObject linkObj = jsonValue.getAsObject();
						String rel = linkObj.getString("rel", null);
						String href = linkObj.getString("href", null);
						theState.attributeValue("rel", rel);
						theState.attributeValue("href", href);
						theState.endingElement();
					}
					continue;
				} else if (BUNDLE_TEXTNODE_CHILDREN_DSTU1.contains(nextName)) {
					theState.enteringNewElement(null, nextName);
					JsonLikeValue jsonElement = theObject.get(nextName);
					if (jsonElement.isScalar()) {
						theState.string(jsonElement.getAsString());
					}
					theState.endingElement();
					continue;
				}
			} else {
				if ("link".equals(nextName)) {
					JsonLikeArray entries = grabJsonArray(theObject, nextName, "link");
					for (int i = 0; i < entries.size(); i++) {
						JsonLikeValue jsonValue = entries.get(i);
						theState.enteringNewElement(null, "link");
						JsonLikeObject linkObj = jsonValue.getAsObject();
						String rel = linkObj.getString("relation", null);
						String href = linkObj.getString("url", null);
						theState.enteringNewElement(null, "relation");
						theState.attributeValue("value", rel);
						theState.endingElement();
						theState.enteringNewElement(null, "url");
						theState.attributeValue("value", href);
						theState.endingElement();
						theState.endingElement();
					}
					continue;
				} else if (BUNDLE_TEXTNODE_CHILDREN_DSTU2.contains(nextName)) {
					theState.enteringNewElement(null, nextName);
					// String obj = theObject.getString(nextName, null);

					JsonLikeValue obj = theObject.get(nextName);
					if (obj == null || obj.isNull()) {
						theState.attributeValue("value", null);
					} else if (obj.isScalar()) {
						theState.attributeValue("value", obj.getAsString());
					} else {
						throw new DataFormatException("Unexpected JSON object for entry '" + nextName + "'");
					}

					theState.endingElement();
					continue;
				}
			}

			JsonLikeValue nextVal = theObject.get(nextName);
			parseChildren(theState, nextName, nextVal, null, null, false);

		}
	}

	private void parseChildren(JsonLikeObject theObject, ParserState<?> theState) {
		Set<String> keySet = theObject.keySet();

		int allUnderscoreNames = 0;
		int handledUnderscoreNames = 0;

		for (String nextName : keySet) {
			if ("resourceType".equals(nextName)) {
				continue;
			} else if ("extension".equals(nextName)) {
				JsonLikeArray array = grabJsonArray(theObject, nextName, "extension");
				parseExtension(theState, array, false);
				continue;
			} else if ("modifierExtension".equals(nextName)) {
				JsonLikeArray array = grabJsonArray(theObject, nextName, "modifierExtension");
				parseExtension(theState, array, true);
				continue;
			} else if (nextName.equals("fhir_comments")) {
				parseFhirComments(theObject.get(nextName), theState);
				continue;
			} else if (nextName.charAt(0) == '_') {
				allUnderscoreNames++;
				continue;
			}

			JsonLikeValue nextVal = theObject.get(nextName);
			String alternateName = '_' + nextName;
			JsonLikeValue alternateVal = theObject.get(alternateName);
			if (alternateVal != null) {
				handledUnderscoreNames++;
			}
			
			parseChildren(theState, nextName, nextVal, alternateVal, alternateName, false);

		}

//		if (elementId != null) {
//			IBase object = (IBase) theState.getObject();
//			if (object instanceof IIdentifiableElement) {
//				((IIdentifiableElement) object).setElementSpecificId(elementId);
//			} else if (object instanceof IBaseResource) {
//				((IBaseResource) object).getIdElement().setValue(elementId);
//			}
//		}

		/*
		 * This happens if an element has an extension but no actual value. I.e.
		 * if a resource has a "_status" element but no corresponding "status"
		 * element. This could be used to handle a null value with an extension
		 * for example.
		 */
		if (allUnderscoreNames > handledUnderscoreNames) {
			for (String alternateName : keySet) {
				if (alternateName.startsWith("_") && alternateName.length() > 1) {
					JsonLikeValue nextValue = theObject.get(alternateName);
					if (nextValue != null) {
						if (nextValue.isObject()) {
							String nextName = alternateName.substring(1);
							if (theObject.get(nextName) == null) {
								theState.enteringNewElement(null, nextName);
								parseAlternates(nextValue, theState, alternateName, alternateName);
								theState.endingElement();
							}
						} else {
							getErrorHandler().incorrectJsonType(null, alternateName, ValueType.OBJECT, null, nextValue.getJsonType(), null);
						}
					}
				}
			}
		}

	}

	private void parseChildren(ParserState<?> theState, String theName, JsonLikeValue theJsonVal, JsonLikeValue theAlternateVal, String theAlternateName, boolean theInArray) {
		if (theName.equals("id")) {
			if (!theJsonVal.isString()) {
				getErrorHandler().incorrectJsonType(null, "id", ValueType.SCALAR, ScalarType.STRING, theJsonVal.getJsonType(), theJsonVal.getDataType());
			}
		}

		if (theJsonVal.isArray()) {
			JsonLikeArray nextArray = theJsonVal.getAsArray();

			JsonLikeValue alternateVal = theAlternateVal;
			if (alternateVal != null && alternateVal.isArray() == false) {
				getErrorHandler().incorrectJsonType(null, theAlternateName, ValueType.ARRAY, null, alternateVal.getJsonType(), null);
				alternateVal = null;
			}

			JsonLikeArray nextAlternateArray = JsonLikeValue.asArray(alternateVal); // could be null
			for (int i = 0; i < nextArray.size(); i++) {
				JsonLikeValue nextObject = nextArray.get(i);
				JsonLikeValue nextAlternate = null;
				if (nextAlternateArray != null) {
					nextAlternate = nextAlternateArray.get(i);
				}
				parseChildren(theState, theName, nextObject, nextAlternate, theAlternateName, true);
			}
		} else if (theJsonVal.isObject()) {
			if (!theInArray && theState.elementIsRepeating(theName)) {
				getErrorHandler().incorrectJsonType(null, theName, ValueType.ARRAY, null, ValueType.OBJECT, null);
			}
			
			theState.enteringNewElement(null, theName);
			parseAlternates(theAlternateVal, theState, theAlternateName, theAlternateName);
			JsonLikeObject nextObject = theJsonVal.getAsObject();
			boolean preResource = false;
			if (theState.isPreResource()) {
				JsonLikeValue resType = nextObject.get("resourceType");
				if (resType == null || !resType.isString()) {
					throw new DataFormatException("Missing required element 'resourceType' from JSON resource object, unable to parse");
				}
				theState.enteringNewElement(null, resType.getAsString());
				preResource = true;
			}
			parseChildren(nextObject, theState);
			if (preResource) {
				theState.endingElement();
			}
			theState.endingElement();
		} else if (theJsonVal.isNull()) {
			theState.enteringNewElement(null, theName);
			parseAlternates(theAlternateVal, theState, theAlternateName, theAlternateName);
			theState.endingElement();
		} else {
			// must be a SCALAR
			theState.enteringNewElement(null, theName);
			theState.attributeValue("value", theJsonVal.getAsString());
			parseAlternates(theAlternateVal, theState, theAlternateName, theAlternateName);
			theState.endingElement();
		}
	}

	private void parseExtension(ParserState<?> theState, JsonLikeArray theValues, boolean theIsModifier) {
		for (int i = 0; i < theValues.size(); i++) {
			JsonLikeObject nextExtObj = JsonLikeValue.asObject(theValues.get(i));
			JsonLikeValue jsonElement = nextExtObj.get("url");
			String url;
			if (null == jsonElement || !(jsonElement.isScalar())) {
				String parentElementName;
				if (theIsModifier) {
					parentElementName = "modifierExtension";
				} else {
					parentElementName = "extension";
				}
				getErrorHandler().missingRequiredElement(new ParseLocation(parentElementName), "url");
				url = null;
			} else {
				url = jsonElement.getAsString();
			}
			theState.enteringNewElementExtension(null, url, theIsModifier);
			for (String next : nextExtObj.keySet()) {
				if ("url".equals(next)) {
					continue;
				} else if ("extension".equals(next)) {
					JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
					parseExtension(theState, jsonVal, false);
				} else if ("modifierExtension".equals(next)) {
					JsonLikeArray jsonVal = JsonLikeValue.asArray(nextExtObj.get(next));
					parseExtension(theState, jsonVal, true);
				} else {
					JsonLikeValue jsonVal = nextExtObj.get(next);
					parseChildren(theState, next, jsonVal, null, null, false);
				}
			}
			theState.endingElement();
		}
	}

	private void parseFhirComments(JsonLikeValue theObject, ParserState<?> theState) {
		if (theObject.isArray()) {
			JsonLikeArray comments = theObject.getAsArray();
			for (int i = 0; i < comments.size(); i++) {
				JsonLikeValue nextComment = comments.get(i);
				if (nextComment.isString()) {
					String commentText = nextComment.getAsString();
					if (commentText != null) {
						theState.commentPre(commentText);
					}
				}
			}
		}
	}

	@Override
	public <T extends IBaseResource> T parseResource(Class<T> theResourceType, JsonLikeStructure theJsonLikeStructure) throws DataFormatException {
		
		/*****************************************************
		 * ************************************************* *
		 * ** NOTE: this duplicates most of the code in   ** *
		 * ** BaseParser.parseResource(Class<T>, Reader). ** *
		 * ** Unfortunately, there is no way to avoid     ** *   
		 * ** this without doing some refactoring of the  ** *
		 * ** BaseParser class.                           ** *
		 * ************************************************* *
		 *****************************************************/
		
		/*
		 * We do this so that the context can verify that the structure is for
		 * the correct FHIR version
		 */
		if (theResourceType != null) {
			myContext.getResourceDefinition(theResourceType);
		}

		// Actually do the parse
		T retVal = doParseResource(theResourceType, theJsonLikeStructure);

		RuntimeResourceDefinition def = myContext.getResourceDefinition(retVal);
		if ("Bundle".equals(def.getName())) {

			BaseRuntimeChildDefinition entryChild = def.getChildByName("entry");
			BaseRuntimeElementCompositeDefinition<?> entryDef = (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry");
			List<IBase> entries = entryChild.getAccessor().getValues(retVal);
			if (entries != null) {
				for (IBase nextEntry : entries) {

					/**
					 * If Bundle.entry.fullUrl is populated, set the resource ID to that
					 */
					// TODO: should emit a warning and maybe notify the error handler if the resource ID doesn't match the
					// fullUrl idPart
					BaseRuntimeChildDefinition fullUrlChild = entryDef.getChildByName("fullUrl");
					if (fullUrlChild == null) {
						continue; // TODO: remove this once the data model in tinder plugin catches up to 1.2
					}
					List<IBase> fullUrl = fullUrlChild.getAccessor().getValues(nextEntry);
					if (fullUrl != null && !fullUrl.isEmpty()) {
						IPrimitiveType<?> value = (IPrimitiveType<?>) fullUrl.get(0);
						if (value.isEmpty() == false) {
							List<IBase> entryResources = entryDef.getChildByName("resource").getAccessor().getValues(nextEntry);
							if (entryResources != null && entryResources.size() > 0) {
								IBaseResource res = (IBaseResource) entryResources.get(0);
								String versionId = res.getIdElement().getVersionIdPart();
								res.setId(value.getValueAsString());
								if (isNotBlank(versionId) && res.getIdElement().hasVersionIdPart() == false) {
									res.setId(res.getIdElement().withVersion(versionId));
								}
							}
						}
					}

				}
			}

		}

		return retVal;
	}

	@Override
	public IBaseResource parseResource(JsonLikeStructure theJsonLikeStructure) throws DataFormatException {
		return parseResource(null, theJsonLikeStructure);
	}

	@Override
	public TagList parseTagList(Reader theReader) {
		JsonLikeStructure jsonStructure = new GsonStructure();
		jsonStructure.load(theReader);
		
		TagList retVal = parseTagList(jsonStructure);
		
		return retVal;
	}
	
	@Override
	public TagList parseTagList(JsonLikeStructure theJsonStructure) {
		JsonLikeObject object = theJsonStructure.getRootObject();

		JsonLikeValue resourceTypeObj = object.get("resourceType");
		String resourceType = resourceTypeObj.getAsString();

		ParserState<TagList> state = ParserState.getPreTagListInstance(this, myContext, true, getErrorHandler());
		state.enteringNewElement(null, resourceType);

		parseChildren(object, state);

		state.endingElement();
		state.endingElement();

		return state.getObject();
	}

	@Override
	public IParser setPrettyPrint(boolean thePrettyPrint) {
		myPrettyPrint = thePrettyPrint;
		return this;
	}

	private void write(JsonLikeWriter theEventWriter, String theChildName, Boolean theValue) throws IOException {
		if (theValue != null) {
			theEventWriter.write(theChildName, theValue.booleanValue());
		}
	}

	// private void parseExtensionInDstu2Style(boolean theModifier, ParserState<?> theState, String
	// theParentExtensionUrl, String theExtensionUrl, JsonArray theValues) {
	// String extUrl = UrlUtil.constructAbsoluteUrl(theParentExtensionUrl, theExtensionUrl);
	// theState.enteringNewElementExtension(null, extUrl, theModifier);
	//
	// for (int extIdx = 0; extIdx < theValues.size(); extIdx++) {
	// JsonObject nextExt = theValues.getJsonObject(extIdx);
	// for (String nextKey : nextExt.keySet()) {
	// // if (nextKey.startsWith("value") && nextKey.length() > 5 &&
	// // myContext.getRuntimeChildUndeclaredExtensionDefinition().getChildByName(nextKey) != null) {
	// JsonElement jsonVal = nextExt.get(nextKey);
	// if (jsonVal.getValueType() == ValueType.ARRAY) {
	// /*
	// * Extension children which are arrays are sub-extensions. Any other value type should be treated as a value.
	// */
	// JsonArray arrayValue = (JsonArray) jsonVal;
	// parseExtensionInDstu2Style(theModifier, theState, extUrl, nextKey, arrayValue);
	// } else {
	// parseChildren(theState, nextKey, jsonVal, null, null);
	// }
	// }
	// }
	//
	// theState.endingElement();
	// }

	private void write(JsonLikeWriter theEventWriter, String theChildName, BigDecimal theDecimalValue) throws IOException {
		theEventWriter.write(theChildName, theDecimalValue);
	}

	private void write(JsonLikeWriter theEventWriter, String theChildName, Integer theValue) throws IOException {
		theEventWriter.write(theChildName, theValue);
	}

	private boolean writeAtomLinkInDstu1Format(JsonLikeWriter theEventWriter, String theRel, StringDt theLink, boolean theStarted) throws IOException {
		boolean retVal = theStarted;
		if (isNotBlank(theLink.getValue())) {
			if (theStarted == false) {
				theEventWriter.beginArray("link");
				retVal = true;
			}

			theEventWriter.beginObject();
			write(theEventWriter, "rel", theRel);
			write(theEventWriter, "href", theLink.getValue());
			theEventWriter.endObject();
		}
		return retVal;
	}

	private boolean writeAtomLinkInDstu2Format(JsonLikeWriter theEventWriter, String theRel, StringDt theLink, boolean theStarted) throws IOException {
		boolean retVal = theStarted;
		if (isNotBlank(theLink.getValue())) {
			if (theStarted == false) {
				theEventWriter.beginArray("link");
				retVal = true;
			}

			theEventWriter.beginObject();
			write(theEventWriter, "relation", theRel);
			write(theEventWriter, "url", theLink.getValue());
			theEventWriter.endObject();
		}
		return retVal;
	}

	private void writeAuthor(BaseBundle theBundle, JsonLikeWriter theEventWriter) throws IOException {
		if (StringUtils.isNotBlank(theBundle.getAuthorName().getValue())) {
			beginArray(theEventWriter, "author");
			theEventWriter.beginObject();
			writeTagWithTextNode(theEventWriter, "name", theBundle.getAuthorName());
			writeOptionalTagWithTextNode(theEventWriter, "uri", theBundle.getAuthorUri());
			theEventWriter.endObject();
			theEventWriter.endArray();
		}
	}

	private void writeCategories(JsonLikeWriter theEventWriter, TagList categories) throws IOException {
		if (categories != null && categories.size() > 0) {
			theEventWriter.beginArray("category");
			for (Tag next : categories) {
				theEventWriter.beginObject();
				write(theEventWriter, "term", defaultString(next.getTerm()));
				write(theEventWriter, "label", defaultString(next.getLabel()));
				write(theEventWriter, "scheme", defaultString(next.getScheme()));
				theEventWriter.endObject();
			}
			theEventWriter.endArray();
		}
	}

	private void writeCommentsPreAndPost(IBase theNextValue, JsonLikeWriter theEventWriter) throws IOException {
		if (theNextValue.hasFormatComment()) {
			beginArray(theEventWriter, "fhir_comments");
			List<String> pre = theNextValue.getFormatCommentsPre();
			if (pre.isEmpty() == false) {
				for (String next : pre) {
					theEventWriter.write(next);
				}
			}
			List<String> post = theNextValue.getFormatCommentsPost();
			if (post.isEmpty() == false) {
				for (String next : post) {
					theEventWriter.write(next);
				}
			}
			theEventWriter.endArray();
		}
	}

	private void writeExtensionsAsDirectChild(IBaseResource theResource, JsonLikeWriter theEventWriter, RuntimeResourceDefinition resDef, List<HeldExtension> extensions, List<HeldExtension> modifierExtensions) throws IOException {
		if (extensions.isEmpty() == false) {
			beginArray(theEventWriter, "extension");
			for (HeldExtension next : extensions) {
				next.write(resDef, theResource, theEventWriter);
			}
			theEventWriter.endArray();
		}
		if (modifierExtensions.isEmpty() == false) {
			beginArray(theEventWriter, "modifierExtension");
			for (HeldExtension next : modifierExtensions) {
				next.write(resDef, theResource, theEventWriter);
			}
			theEventWriter.endArray();
		}
	}

	private void writeOptionalTagWithDecimalNode(JsonLikeWriter theEventWriter, String theElementName, DecimalDt theValue) throws IOException {
		if (theValue != null && theValue.isEmpty() == false) {
			write(theEventWriter, theElementName, theValue.getValue());
		}
	}

	private void writeOptionalTagWithNumberNode(JsonLikeWriter theEventWriter, String theElementName, IntegerDt theValue) throws IOException {
		if (theValue != null && theValue.isEmpty() == false) {
			write(theEventWriter, theElementName, theValue.getValue().intValue());
		}
	}

	private void writeOptionalTagWithTextNode(JsonLikeWriter theEventWriter, String theElementName, IPrimitiveDatatype<?> thePrimitive) throws IOException {
		if (thePrimitive == null) {
			return;
		}
		String str = thePrimitive.getValueAsString();
		writeOptionalTagWithTextNode(theEventWriter, theElementName, str);
	}

	private void writeOptionalTagWithTextNode(JsonLikeWriter theEventWriter, String theElementName, String theValue) throws IOException {
		if (StringUtils.isNotBlank(theValue)) {
			write(theEventWriter, theElementName, theValue);
		}
	}

	private void writeTagWithTextNode(JsonLikeWriter theEventWriter, String theElementName, IPrimitiveDatatype<?> theIdDt) throws IOException {
		if (theIdDt != null && !theIdDt.isEmpty()) {
			write(theEventWriter, theElementName, theIdDt.getValueAsString());
		} else {
			theEventWriter.writeNull(theElementName);
		}
	}

	private void writeTagWithTextNode(JsonLikeWriter theEventWriter, String theElementName, StringDt theStringDt) throws IOException {
		if (StringUtils.isNotBlank(theStringDt.getValue())) {
			write(theEventWriter, theElementName, theStringDt.getValue());
		}
		// else {
		// theEventWriter.writeNull(theElementName);
		// }
	}

	public static Gson newGson() {
		Gson gson = new GsonBuilder().disableHtmlEscaping().create();
		return gson;
	}
	
	private static void write(JsonLikeWriter theWriter, String theName, String theValue) throws IOException {
		theWriter.write(theName, theValue);
	}
	
	private class HeldExtension implements Comparable<HeldExtension> {

		private CompositeChildElement myChildElem;
		private RuntimeChildDeclaredExtensionDefinition myDef;
		private boolean myModifier;
		private IBaseExtension<?, ?> myUndeclaredExtension;
		private IBase myValue;

		public HeldExtension(IBaseExtension<?, ?> theUndeclaredExtension, boolean theModifier, CompositeChildElement theChildElem) {
			assert theUndeclaredExtension != null;
			myUndeclaredExtension = theUndeclaredExtension;
			myModifier = theModifier;
			myChildElem = theChildElem;
		}

		public HeldExtension(RuntimeChildDeclaredExtensionDefinition theDef, IBase theValue, CompositeChildElement theChildElem) {
			assert theDef != null;
			assert theValue != null;
			myDef = theDef;
			myValue = theValue;
			myChildElem = theChildElem;
		}

		@Override
		public int compareTo(HeldExtension theArg0) {
			String url1 = myDef != null ? myDef.getExtensionUrl() : myUndeclaredExtension.getUrl();
			String url2 = theArg0.myDef != null ? theArg0.myDef.getExtensionUrl() : theArg0.myUndeclaredExtension.getUrl();
			url1 = defaultString(url1);
			url2 = defaultString(url2);
			return url1.compareTo(url2);
		}

		public void write(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter) throws IOException {
			if (myUndeclaredExtension != null) {
				writeUndeclaredExtension(theResDef, theResource, theEventWriter, myUndeclaredExtension);
			} else {
				theEventWriter.beginObject();

				writeCommentsPreAndPost(myValue, theEventWriter);

				JsonParser.write(theEventWriter, "url", myDef.getExtensionUrl());

				/*
				 * This makes sure that even if the extension contains a reference to a contained
				 * resource which has a HAPI-assigned ID we'll still encode that ID.
				 * 
				 * See #327 
				 */
				List<? extends IBase> preProcessedValue = preProcessValues(myDef, theResource, Collections.singletonList(myValue), myChildElem);
				myValue = preProcessedValue.get(0);
				
				BaseRuntimeElementDefinition<?> def = myDef.getChildElementDefinitionByDatatype(myValue.getClass());
				if (def.getChildType() == ChildTypeEnum.RESOURCE_BLOCK) {
					extractAndWriteExtensionsAsDirectChild(myValue, theEventWriter, def, theResDef, theResource, myChildElem);
				} else {
					String childName = myDef.getChildNameByDatatype(myValue.getClass());
					encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, myValue, def, childName, false, null, false);
				}

				theEventWriter.endObject();
			}
		}

		
		private void writeUndeclaredExtension(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonLikeWriter theEventWriter, IBaseExtension<?, ?> ext) throws IOException {
			IBase value = ext.getValue();
			String extensionUrl = ext.getUrl();

			theEventWriter.beginObject();

			writeCommentsPreAndPost(myUndeclaredExtension, theEventWriter);

			String elementId = getCompositeElementId(ext);
			if (isNotBlank(elementId)) {
				JsonParser.write(theEventWriter, "id", getCompositeElementId(ext));
			}

			JsonParser.write(theEventWriter, "url", extensionUrl);

			boolean noValue = value == null || value.isEmpty();
			if (noValue && ext.getExtension().isEmpty()) {
				ourLog.debug("Extension with URL[{}] has no value", extensionUrl);
			} else if (noValue) {

				if (myModifier) {
					beginArray(theEventWriter, "modifierExtension");
				} else {
					beginArray(theEventWriter, "extension");
				}

				for (Object next : ext.getExtension()) {
					writeUndeclaredExtension(theResDef, theResource, theEventWriter, (IBaseExtension<?, ?>) next);
				}
				theEventWriter.endArray();
			} else {

				/*
				 * Pre-process value - This is called in case the value is a reference
				 * since we might modify the text
				 */
				value = JsonParser.super.preProcessValues(myDef, theResource, Collections.singletonList(value), myChildElem).get(0);

				RuntimeChildUndeclaredExtensionDefinition extDef = myContext.getRuntimeChildUndeclaredExtensionDefinition();
				String childName = extDef.getChildNameByDatatype(value.getClass());
				if (childName == null) {
					childName = "value" + WordUtils.capitalize(myContext.getElementDefinition(value.getClass()).getName());
				}
				BaseRuntimeElementDefinition<?> childDef = extDef.getChildElementDefinitionByDatatype(value.getClass());
				if (childDef == null) {
					throw new ConfigurationException("Unable to encode extension, unregognized child element type: " + value.getClass().getCanonicalName());
				}
				encodeChildElementToStreamWriter(theResDef, theResource, theEventWriter, value, childDef, childName, true, null, false);
			}

			// theEventWriter.name(myUndeclaredExtension.get);

			theEventWriter.endObject();
		}

	}

}
