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.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;

import java.util.*;

import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.*;

import ca.uhn.fhir.context.*;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IMutator;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
import ca.uhn.fhir.model.base.resource.ResourceMetadataMap;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.XhtmlDt;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.IModelVisitor;
import ca.uhn.fhir.util.ReflectionUtil;

class ParserState<T> {

	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ParserState.class);

	private List<String> myComments = new ArrayList<String>(2);
	private final FhirContext myContext;
	private final IParserErrorHandler myErrorHandler;
	private final boolean myJsonMode;
	private T myObject;
	private final IParser myParser;
	private IBase myPreviousElement;
	private BaseState myState;

	private ParserState(IParser theParser, FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) {
		myParser = theParser;
		myContext = theContext;
		myJsonMode = theJsonMode;
		myErrorHandler = theErrorHandler;
	}

	public void attributeValue(String theName, String theValue) throws DataFormatException {
		myState.attributeValue(theName, theValue);
	}

	public void commentPost(String theCommentText) {
		if (myPreviousElement != null) {
			myPreviousElement.getFormatCommentsPost().add(theCommentText);
		}
	}

	public void commentPre(String theCommentText) {
		if (myState.getCurrentElement() != null) {
			IBase element = myState.getCurrentElement();
			element.getFormatCommentsPre().add(theCommentText);
		}
	}

	public boolean elementIsRepeating(String theChildName) {
		return myState.elementIsRepeating(theChildName);
	}

	public void endingElement() throws DataFormatException {
		myState.endingElement();
	}

	public void enteringNewElement(String theNamespaceUri, String theName) throws DataFormatException {
		myState.enteringNewElement(theNamespaceUri, theName);
	}

	public void enteringNewElementExtension(StartElement theElem, String theUrlAttr, boolean theIsModifier) {
		myState.enteringNewElementExtension(theElem, theUrlAttr, theIsModifier);
	}

	public T getObject() {
		return myObject;
	}

	public boolean isPreResource() {
		return myState.isPreResource();
	}

	private Object newContainedDt(IResource theTarget) {
		return ReflectionUtil.newInstance(theTarget.getStructureFhirVersionEnum().getVersionImplementation().getContainedType());
	}

	@SuppressWarnings("unchecked")
	private void pop() {
		myPreviousElement = myState.getCurrentElement();
		if (myState.myStack != null) {
			myState = myState.myStack;
			myState.wereBack();
		} else {
			myObject = (T) myState.getCurrentElement();
			myState = null;
		}
	}

	private void push(BaseState theState) {
		theState.setStack(myState);
		myState = theState;
		if (myComments.isEmpty() == false) {
			if (myState.getCurrentElement() != null) {
				myState.getCurrentElement().getFormatCommentsPre().addAll(myComments);
				myComments.clear();
			}
		}
	}

	private void putPlacerResourceInDeletedEntry(BundleEntry entry) {
		IdDt id = null;
		if (entry.getLinkSelf() != null && entry.getLinkSelf().isEmpty() == false) {
			id = new IdDt(entry.getLinkSelf().getValue());
		} else {
			//TODO: Use of a deprecated method should be resolved.
			id = entry.getId();
		}

		IResource resource = entry.getResource();
		if (resource == null && id != null && isNotBlank(id.getResourceType())) {
			String resourceType = id.getResourceType();
			RuntimeResourceDefinition def = myContext.getResourceDefinition(resourceType);
			if (def == null) {
				throw new DataFormatException("Entry references unknown resource type: " + resourceType);
			}
			resource = (IResource) def.newInstance();
			resource.setId(id);
			entry.setResource(resource);
		}

		if (resource != null) {
			resource.getResourceMetadata().put(ResourceMetadataKeyEnum.DELETED_AT, entry.getDeletedAt());
			//TODO: Use of a deprecated method should be resolved.
			resource.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, id);
		}
	}

	public void string(String theData) {
		myState.string(theData);
	}

	public boolean verifyNamespace(String theExpect, String theActual) {
		if (myJsonMode) {
			return true;
		}
		return StringUtils.equals(theExpect, theActual);
	}

	/**
	 * Invoked after any new XML event is individually processed, containing a copy of the XML event. This is basically
	 * intended for embedded XHTML content
	 */
	public void xmlEvent(XMLEvent theNextEvent) {
		if (myState != null) {
			myState.xmlEvent(theNextEvent);
		}
	}

	static ParserState<Bundle> getPreAtomInstance(IParser theParser, FhirContext theContext, Class<? extends IBaseResource> theResourceType, boolean theJsonMode, IParserErrorHandler theErrorHandler)
			throws DataFormatException {
		ParserState<Bundle> retVal = new ParserState<Bundle>(theParser, theContext, theJsonMode, theErrorHandler);
		if (theContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) {
			retVal.push(retVal.new PreAtomState(theResourceType));
		} else {
			retVal.push(retVal.new PreBundleState(theResourceType));
		}
		return retVal;
	}

	/**
	 * @param theResourceType
	 *           May be null
	 */
	static <T extends IBaseResource> ParserState<T> getPreResourceInstance(IParser theParser, Class<T> theResourceType, FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler)
			throws DataFormatException {
		ParserState<T> retVal = new ParserState<T>(theParser, theContext, theJsonMode, theErrorHandler);
		if (theResourceType == null) {
			if (theContext.getVersion().getVersion().isRi()) {
				retVal.push(retVal.new PreResourceStateHl7Org(theResourceType));
			} else {
				retVal.push(retVal.new PreResourceStateHapi(theResourceType));
			}
		} else {
			if (IResource.class.isAssignableFrom(theResourceType)) {
				retVal.push(retVal.new PreResourceStateHapi(theResourceType));
			} else {
				retVal.push(retVal.new PreResourceStateHl7Org(theResourceType));
			}
		}
		return retVal;
	}

	static ParserState<TagList> getPreTagListInstance(IParser theParser, FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) {
		ParserState<TagList> retVal = new ParserState<TagList>(theParser, theContext, theJsonMode, theErrorHandler);
		retVal.push(retVal.new PreTagListState());
		return retVal;
	}

	private class AtomAuthorState extends BaseState {

		private BaseBundle myInstance;

		public AtomAuthorState(BaseBundle theEntry) {
			super(null);
			myInstance = theEntry;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("name".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myInstance.getAuthorName()));
			} else if ("uri".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myInstance.getAuthorUri()));
			} else {
				throw new DataFormatException("Unexpected element: " + theLocalPart);
			}
		}

	}

	public class AtomCategoryState extends BaseState {

		private static final int STATE_LABEL = 2;
		private static final int STATE_NONE = 0;
		private static final int STATE_SCHEME = 3;
		private static final int STATE_TERM = 1;

		private int myCatState = STATE_NONE;
		private String myLabel;
		private String myScheme;
		private TagList myTagList;
		private String myTerm;

		public AtomCategoryState(TagList theTagList) {
			super(null);
			myTagList = theTagList;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if ("term".equals(theName)) {
				myTerm = theValue;
			} else if ("label".equals(theName)) {
				myLabel = theValue;
			} else if ("scheme".equals(theName)) {
				myScheme = theValue;
			} else if ("value".equals(theName)) {
				/*
				 * This handles XML parsing, which is odd for this quasi-resource type, since the tag has three values
				 * instead of one like everything else.
				 */
				switch (myCatState) {
				case STATE_LABEL:
					myLabel = theValue;
					break;
				case STATE_TERM:
					myTerm = theValue;
					break;
				case STATE_SCHEME:
					myScheme = theValue;
					break;
				default:
					super.string(theValue);
					break;
				}

			}
		}

		@Override
		public void endingElement() throws DataFormatException {
			if (myCatState != STATE_NONE) {
				myCatState = STATE_NONE;
			} else {
				if (isNotEmpty(myScheme) || isNotBlank(myTerm) || isNotBlank(myLabel)) {
					myTagList.addTag(myScheme, myTerm, myLabel);
				}
				pop();
			}
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theName) throws DataFormatException {
			if (myCatState != STATE_NONE) {
				throw new DataFormatException("Unexpected element in entry: " + theName);
			}
			if ("term".equals(theName)) {
				myCatState = STATE_TERM;
			} else if ("label".equals(theName)) {
				myCatState = STATE_LABEL;
			} else if ("scheme".equals(theName)) {
				myCatState = STATE_SCHEME;
			}
		}

	}

	private class AtomDeletedEntryByState extends BaseState {

		private BundleEntry myEntry;

		public AtomDeletedEntryByState(BundleEntry theEntry) {
			super(null);
			myEntry = theEntry;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("name".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myEntry.getDeletedByName()));
			} else if ("email".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myEntry.getDeletedByEmail()));
			} else {
				throw new DataFormatException("Unexpected element in entry: " + theLocalPart);
			}
		}

	}

	private class AtomDeletedEntryState extends AtomEntryState {

		public AtomDeletedEntryState(Bundle theInstance, Class<? extends IBaseResource> theResourceType) {
			super(theInstance, theResourceType);
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if ("ref".equals(theName)) {
				//TODO: Use of a deprecated method should be resolved.
				getEntry().setId(new IdDt(theValue));
			} else if ("when".equals(theName)) {
				getEntry().setDeleted(new InstantDt(theValue));
			}
		}

		@Override
		public void endingElement() throws DataFormatException {
			putPlacerResourceInDeletedEntry(getEntry());
			super.endingElement();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("by".equals(theLocalPart) && verifyNamespace(XmlParser.TOMBSTONES_NS, theNamespaceUri)) {
				push(new AtomDeletedEntryByState(getEntry()));
			} else if ("comment".equals(theLocalPart)) {
				push(new AtomPrimitiveState(getEntry().getDeletedComment()));
			} else {
				super.enteringNewElement(theNamespaceUri, theLocalPart);
			}
		}

	}

	private class AtomDeletedJsonWhenState extends BaseState {

		private String myData;
		private IPrimitiveDatatype<?> myPrimitive;

		public AtomDeletedJsonWhenState(IPrimitiveDatatype<?> thePrimitive) {
			super(null);
			Validate.notNull(thePrimitive, "thePrimitive");
			myPrimitive = thePrimitive;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			myData = theValue;
		}

		@Override
		public void endingElement() throws DataFormatException {
			myPrimitive.setValueAsString(myData);
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			throw new DataFormatException("Unexpected nested element in atom tag: " + theLocalPart);
		}

		@Override
		protected IElement getCurrentElement() {
			return null;
		}

	}

	private class AtomEntryState extends BaseState {

		private boolean myDeleted;
		private BundleEntry myEntry;
		private Class<? extends IBaseResource> myResourceType;

		public AtomEntryState(Bundle theInstance, Class<? extends IBaseResource> theResourceType) {
			super(null);
			myEntry = new BundleEntry();
			myResourceType = theResourceType;
			theInstance.getEntries().add(myEntry);
		}

		@Override
		public void endingElement() throws DataFormatException {
			populateResourceMetadata();
			pop();

			if (myDeleted) {
				putPlacerResourceInDeletedEntry(myEntry);
			}
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("title".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myEntry.getTitle()));
			} else if ("id".equals(theLocalPart)) {
				//TODO: Use of a deprecated method should be resolved.
				push(new AtomPrimitiveState(myEntry.getId()));
			} else if ("link".equals(theLocalPart)) {
				push(new AtomLinkState(myEntry));
			} else if ("updated".equals(theLocalPart)) {
				//TODO: Use of a deprecated method should be resolved.
				push(new AtomPrimitiveState(myEntry.getUpdated()));
			} else if ("published".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myEntry.getPublished()));
			} else if ("author".equals(theLocalPart)) {
				push(new AtomAuthorState(myEntry));
			} else if ("content".equals(theLocalPart)) {
				push(new PreResourceStateHapi(myEntry, myResourceType).setRequireResourceType(false));
			} else if ("summary".equals(theLocalPart)) {
				push(new XhtmlState(getPreResourceState(), myEntry.getSummary(), false));
			} else if ("category".equals(theLocalPart)) {
				push(new AtomCategoryState(myEntry.getCategories()));
			} else if ("deleted".equals(theLocalPart) && myJsonMode) {
				// JSON and XML deleted entries are completely different for some reason
				myDeleted = true;
				push(new AtomDeletedJsonWhenState(myEntry.getDeletedAt()));
			} else {
				throw new DataFormatException("Unexpected element in entry: " + theLocalPart);
			}

			// TODO: handle category
		}

		protected BundleEntry getEntry() {
			return myEntry;
		}

		@SuppressWarnings("deprecation")
		private void populateResourceMetadata() {
			if (myEntry.getResource() == null) {
				return;
			}

			IdDt id = myEntry.getId();
			if (id != null && id.isEmpty() == false) {
				myEntry.getResource().setId(id);
			}

			Map<ResourceMetadataKeyEnum<?>, Object> metadata = myEntry.getResource().getResourceMetadata();
			if (myEntry.getPublished().isEmpty() == false) {
				ResourceMetadataKeyEnum.PUBLISHED.put(myEntry.getResource(), myEntry.getPublished());
			}
			if (myEntry.getUpdated().isEmpty() == false) {
				ResourceMetadataKeyEnum.UPDATED.put(myEntry.getResource(), myEntry.getUpdated());
			}

			ResourceMetadataKeyEnum.TITLE.put(myEntry.getResource(), myEntry.getTitle().getValue());

			if (myEntry.getCategories().isEmpty() == false) {
				TagList tagList = new TagList();
				for (Tag next : myEntry.getCategories()) {
					tagList.add(next);
				}
				ResourceMetadataKeyEnum.TAG_LIST.put(myEntry.getResource(), tagList);
			}
			if (!myEntry.getLinkSelf().isEmpty()) {
				String linkSelfValue = myEntry.getLinkSelf().getValue();
				IdDt linkSelf = new IdDt(linkSelfValue);
				myEntry.getResource().setId(linkSelf);
				if (isNotBlank(linkSelf.getVersionIdPart())) {
					metadata.put(ResourceMetadataKeyEnum.VERSION_ID, linkSelf);
				}
			}
			if (!myEntry.getLinkAlternate().isEmpty()) {
				ResourceMetadataKeyEnum.LINK_ALTERNATE.put(myEntry.getResource(), myEntry.getLinkAlternate().getValue());
			}
			if (!myEntry.getLinkSearch().isEmpty()) {
				ResourceMetadataKeyEnum.LINK_SEARCH.put(myEntry.getResource(), myEntry.getLinkSearch().getValue());
			}

		}

	}

	private class AtomLinkState extends BaseState {

		private BundleEntry myEntry;
		private String myHref;
		private Bundle myInstance;
		private String myRel;

		public AtomLinkState(Bundle theInstance) {
			super(null);
			myInstance = theInstance;
		}

		public AtomLinkState(BundleEntry theEntry) {
			super(null);
			myEntry = theEntry;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if ("rel".equals(theName)) {
				myRel = theValue;
			} else if ("href".equals(theName)) {
				myHref = theValue;
			}
		}

		@Override
		public void endingElement() throws DataFormatException {
			if (myInstance != null) {
				if ("self".equals(myRel)) {
					myInstance.getLinkSelf().setValueAsString(myHref);
				} else if ("first".equals(myRel)) {
					myInstance.getLinkFirst().setValueAsString(myHref);
				} else if ("previous".equals(myRel)) {
					myInstance.getLinkPrevious().setValueAsString(myHref);
				} else if ("next".equals(myRel)) {
					myInstance.getLinkNext().setValueAsString(myHref);
				} else if ("last".equals(myRel)) {
					myInstance.getLinkLast().setValueAsString(myHref);
				} else if ("fhir-base".equals(myRel)) {
					myInstance.getLinkBase().setValueAsString(myHref);
				}
			} else {
				if ("self".equals(myRel)) {
					myEntry.getLinkSelf().setValueAsString(myHref);
				} else if ("search".equals(myRel)) {
					myEntry.getLinkSearch().setValueAsString(myHref);
				} else if ("alternate".equals(myRel)) {
					myEntry.getLinkAlternate().setValueAsString(myHref);
				}
			}
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			throw new DataFormatException("Found unexpected element content '" + theLocalPart + "' within <link>");
		}

	}

	private class AtomPrimitiveState extends BaseState {

		private String myData;
		private IPrimitiveDatatype<?> myPrimitive;

		public AtomPrimitiveState(IPrimitiveDatatype<?> thePrimitive) {
			super(null);
			Validate.notNull(thePrimitive, "thePrimitive");
			myPrimitive = thePrimitive;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if (myJsonMode) {
				string(theValue);
			} else {
				super.attributeValue(theName, theValue);
			}
		}

		@Override
		public void endingElement() throws DataFormatException {
			myPrimitive.setValueAsString(myData);
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			throw new DataFormatException("Unexpected nested element in atom tag: " + theLocalPart);
		}

		@Override
		protected IElement getCurrentElement() {
			return null;
		}

		@Override
		public void string(String theData) {
			if (myData == null) {
				myData = theData;
			} else {
				// this shouldn't generally happen so it's ok that it's
				// inefficient
				myData = myData + theData;
			}
		}

	}

	private class AtomState extends BaseState {

		private Bundle myInstance;
		private Class<? extends IBaseResource> myResourceType;

		public AtomState(Bundle theInstance, Class<? extends IBaseResource> theResourceType) {
			super(null);
			myInstance = theInstance;
			myResourceType = theResourceType;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("entry".equals(theLocalPart) && verifyNamespace(XmlParser.ATOM_NS, theNamespaceUri)) {
				push(new AtomEntryState(myInstance, myResourceType));
			} else if (theLocalPart.equals("title")) {
				push(new AtomPrimitiveState(myInstance.getTitle()));
			} else if ("id".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myInstance.getBundleId()));
			} else if ("link".equals(theLocalPart)) {
				push(new AtomLinkState(myInstance));
			} else if ("totalResults".equals(theLocalPart) && (verifyNamespace(XmlParser.OPENSEARCH_NS, theNamespaceUri) || verifyNamespace(Constants.OPENSEARCH_NS_OLDER, theNamespaceUri))) {
				push(new AtomPrimitiveState(myInstance.getTotalResults()));
			} else if ("updated".equals(theLocalPart)) {
				push(new AtomPrimitiveState(myInstance.getUpdated()));
			} else if ("author".equals(theLocalPart)) {
				push(new AtomAuthorState(myInstance));
			} else if ("category".equals(theLocalPart)) {
				push(new AtomCategoryState(myInstance.getCategories()));
			} else if ("deleted-entry".equals(theLocalPart) && verifyNamespace(XmlParser.TOMBSTONES_NS, theNamespaceUri)) {
				push(new AtomDeletedEntryState(myInstance, myResourceType));
			} else {
				logAndSwallowUnexpectedElement(theLocalPart);
			}

			// TODO: handle category and DSig
		}

		@Override
		protected IElement getCurrentElement() {
			return myInstance;
		}

	}

	private class BasePreAtomOrBundleState extends BaseState {

		private Bundle myInstance;

		private Class<? extends IBaseResource> myResourceType;

		public BasePreAtomOrBundleState(Class<? extends IBaseResource> theResourceType) {
			super(null);
			myResourceType = theResourceType;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		protected IElement getCurrentElement() {
			return myInstance;
		}

		public Bundle getInstance() {
			return myInstance;
		}

		protected Class<? extends IBaseResource> getResourceType() {
			return myResourceType;
		}

		public void setInstance(Bundle theInstance) {
			myInstance = theInstance;
		}

		@Override
		public void wereBack() {

			/*
			 * Stitch together resource references
			 */

			Map<String, IResource> idToResource = new HashMap<String, IResource>();
			List<IResource> resources = myInstance.toListOfResources();
			for (IResource next : resources) {
				if (next.getId() != null && next.getId().isEmpty() == false) {
					idToResource.put(next.getId().toUnqualifiedVersionless().getValue(), next);
				}
			}

			for (IResource next : resources) {
				List<BaseResourceReferenceDt> refs = myContext.newTerser().getAllPopulatedChildElementsOfType(next, BaseResourceReferenceDt.class);
				for (BaseResourceReferenceDt nextRef : refs) {
					if (nextRef.isEmpty() == false && nextRef.getReference() != null) {
						IResource target = idToResource.get(nextRef.getReference().getValue());
						if (target != null) {
							nextRef.setResource(target);
						}
					}
				}
			}

		}

	}
	
	
	private abstract class BaseState {

		private PreResourceState myPreResourceState;
		private BaseState myStack;

		public BaseState(PreResourceState thePreResourceState) {
			super();
			myPreResourceState = thePreResourceState;
		}

		/**
		 * @param theValue
		 *           The attribute value
		 */
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			myErrorHandler.unknownAttribute(null, theName);
		}

		public boolean elementIsRepeating(String theChildName) {
			return false;
		}

		public void endingElement() throws DataFormatException {
			// ignore by default
		}

		/**
		 * @param theNamespaceUri
		 *           The XML namespace (if XML) or null
		 */
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			myErrorHandler.unknownElement(null, theLocalPart);
		}

		/**
		 * Default implementation just handles undeclared extensions
		 */
		public void enteringNewElementExtension(StartElement theElement, String theUrlAttr, boolean theIsModifier) {
			if (myPreResourceState != null && getCurrentElement() instanceof ISupportsUndeclaredExtensions) {
				ExtensionDt newExtension = new ExtensionDt(theIsModifier);
				newExtension.setUrl(theUrlAttr);
				ISupportsUndeclaredExtensions elem = (ISupportsUndeclaredExtensions) getCurrentElement();
				elem.addUndeclaredExtension(newExtension);
				ExtensionState newState = new ExtensionState(myPreResourceState, newExtension);
				push(newState);
			} else {
				if (theIsModifier == false) {
					if (getCurrentElement() instanceof IBaseHasExtensions) {
						IBaseExtension<?, ?> ext = ((IBaseHasExtensions) getCurrentElement()).addExtension();
						ext.setUrl(theUrlAttr);
						ParserState<T>.ExtensionState newState = new ExtensionState(myPreResourceState, ext);
						push(newState);
					} else {
						logAndSwallowUnexpectedElement("extension");
					}
				} else {
					if (getCurrentElement() instanceof IBaseHasModifierExtensions) {
						IBaseExtension<?, ?> ext = ((IBaseHasModifierExtensions) getCurrentElement()).addModifierExtension();
						ext.setUrl(theUrlAttr);
						ParserState<T>.ExtensionState newState = new ExtensionState(myPreResourceState, ext);
						push(newState);
					} else {
						logAndSwallowUnexpectedElement("modifierExtension");
					}
				}
			}
		}

		protected IBase getCurrentElement() {
			return null;
		}

		public PreResourceState getPreResourceState() {
			return myPreResourceState;
		}

		public boolean isPreResource() {
			return false;
		}

		protected void logAndSwallowUnexpectedElement(String theLocalPart) {
			myErrorHandler.unknownElement(null, theLocalPart);
			push(new SwallowChildrenWholeState(getPreResourceState()));
		}

		public void setStack(BaseState theState) {
			myStack = theState;
		}

		/**
		 * @param theData
		 *           The string value
		 */
		public void string(String theData) {
			// ignore by default
		}

		public void wereBack() {
			// allow an implementor to override
		}

		/**
		 * @param theNextEvent
		 *           The XML event
		 */
		public void xmlEvent(XMLEvent theNextEvent) {
			// ignore
		}

	}

	private class BinaryResourceStateForDstu1 extends BaseState {

		private static final int SUBSTATE_CONTENT = 2;
		private static final int SUBSTATE_CT = 1;
		private String myData;
		private IBaseBinary myInstance;
		private int mySubState = 0;

		public BinaryResourceStateForDstu1(PreResourceState thePreResourceState, IBaseBinary theInstance) {
			super(thePreResourceState);
			myInstance = theInstance;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if ("id".equals(theName)) {
				if (myInstance instanceof IIdentifiableElement) {
					((IIdentifiableElement) myInstance).setElementSpecificId((theValue));
				} else {
					((IResource) myInstance).setId(new IdDt(theValue));
				}
			} else if ("contentType".equals(theName)) {
				myInstance.setContentType(theValue);
			} else if (myJsonMode && "value".equals(theName)) {
				string(theValue);
			}
		}

		@Override
		public void endingElement() throws DataFormatException {
			if (mySubState == SUBSTATE_CT) {
				myInstance.setContentType(myData);
				mySubState = 0;
				myData = null;
				return;
			} else if (mySubState == SUBSTATE_CONTENT) {
				myInstance.setContentAsBase64(myData);
				mySubState = 0;
				myData = null;
				return;
			} else {
				if (!myJsonMode) {
					myInstance.setContentAsBase64(myData);
				}
				pop();
			}
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (myJsonMode && "contentType".equals(theLocalPart) && mySubState == 0) {
				mySubState = SUBSTATE_CT;
			} else if (myJsonMode && "content".equals(theLocalPart) && mySubState == 0) {
				mySubState = SUBSTATE_CONTENT;
			} else {
				throw new DataFormatException("Unexpected nested element in atom tag: " + theLocalPart);
			}
		}

		@Override
		protected IElement getCurrentElement() {
			return null;
		}

		@Override
		public void string(String theData) {
			if (myData == null) {
				myData = theData;
			} else {
				// this shouldn't generally happen so it's ok that it's
				// inefficient
				myData = myData + theData;
			}
		}

	}

	private class BundleEntryDeletedState extends BaseState {

		private BundleEntry myEntry;

		public BundleEntryDeletedState(PreResourceState thePreResourceState, BundleEntry theEntry) {
			super(thePreResourceState);
			myEntry = theEntry;
		}

		@Override
		public void endingElement() throws DataFormatException {
			String resType = myEntry.getDeletedResourceType().getValue();
			String id = myEntry.getDeletedResourceId().getValue();
			String version = myEntry.getDeletedResourceVersion().getValue();
			myEntry.setLinkSelf(new StringDt(new IdDt(resType, id, version).getValue()));
			putPlacerResourceInDeletedEntry(myEntry);
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("type".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getDeletedResourceType()));
			} else if ("id".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getDeletedResourceId()));
			} else if ("resourceId".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getDeletedResourceId()));
			} else if ("version".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getDeletedResourceVersion()));
			} else if ("versionId".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getDeletedResourceVersion()));
			} else if ("instant".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getDeletedAt()));
			} else {
				throw new DataFormatException("Unexpected element '" + theLocalPart + "' in element 'deleted'");
			}
		}

	}

	private class BundleEntrySearchState extends BaseState {

		private BundleEntry myEntry;

		public BundleEntrySearchState(BundleEntry theEntry) {
			super(null);
			myEntry = theEntry;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("mode".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getSearchMode()));
			} else if ("score".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getScore()));
			} else {
				throw new DataFormatException("Unexpected element in Bundle.entry.search: " + theLocalPart);
			}
		}

	}

	private class BundleEntryState extends BaseState {

		private BundleEntry myEntry;
		private IdDt myFullUrl;
		private Class<? extends IBaseResource> myResourceType;

		public BundleEntryState(Bundle theInstance, Class<? extends IBaseResource> theResourceType) {
			super(null);
			myEntry = new BundleEntry();
			myResourceType = theResourceType;
			theInstance.getEntries().add(myEntry);
		}

		@Override
		public void endingElement() throws DataFormatException {
			populateResourceMetadata();
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("base".equals(theLocalPart)) {
				//TODO: Use of a deprecated method should be resolved.
				push(new PrimitiveState(getPreResourceState(), myEntry.getLinkBase()));
			} else if ("request".equals(theLocalPart)) {
				push(new BundleEntryTransactionState(myEntry));
			} else if ("search".equals(theLocalPart)) {
				push(new BundleEntrySearchState(myEntry));
			} else if ("score".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getScore()));
			} else if ("resource".equals(theLocalPart)) {
				push(new PreResourceStateHapi(myEntry, myResourceType).setRequireResourceType(false));
			} else if ("deleted".equals(theLocalPart)) {
				push(new BundleEntryDeletedState(getPreResourceState(), myEntry));
			} else if ("link".equals(theLocalPart)) {
				push(new BundleLinkState(myEntry));
			} else if ("fullUrl".equals(theLocalPart)) {
				myFullUrl = new IdDt();
				push(new PrimitiveState(getPreResourceState(), myFullUrl));
			} else if ("fhir_comments".equals(theLocalPart) && myJsonMode) {
				push(new SwallowChildrenWholeState(getPreResourceState()));
			} else {
				throw new DataFormatException("Unexpected element in entry: " + theLocalPart);
			}
		}

		@SuppressWarnings("deprecation")
		private void populateResourceMetadata() {
			if (myEntry.getResource() == null) {
				return;
			}

			IdDt id = myEntry.getId();
			if (id != null && id.isEmpty() == false) {
				myEntry.getResource().setId(id);
			}

			if (myFullUrl != null && !myFullUrl.isEmpty()) {
				myEntry.getResource().setId(myFullUrl);
			}

			Map<ResourceMetadataKeyEnum<?>, Object> metadata = myEntry.getResource().getResourceMetadata();
			if (myEntry.getPublished().isEmpty() == false) {
				ResourceMetadataKeyEnum.PUBLISHED.put(myEntry.getResource(), myEntry.getPublished());
			}
			if (myEntry.getUpdated().isEmpty() == false) {
				ResourceMetadataKeyEnum.UPDATED.put(myEntry.getResource(), myEntry.getUpdated());
			}

			ResourceMetadataKeyEnum.TITLE.put(myEntry.getResource(), myEntry.getTitle().getValue());

			if (myEntry.getCategories().isEmpty() == false) {
				TagList tagList = new TagList();
				for (Tag next : myEntry.getCategories()) {
					tagList.add(next);
				}
				ResourceMetadataKeyEnum.TAG_LIST.put(myEntry.getResource(), tagList);
			}
			if (!myEntry.getLinkSelf().isEmpty()) {
				String linkSelfValue = myEntry.getLinkSelf().getValue();
				IdDt linkSelf = new IdDt(linkSelfValue);
				myEntry.getResource().setId(linkSelf);
				if (isNotBlank(linkSelf.getVersionIdPart())) {
					metadata.put(ResourceMetadataKeyEnum.VERSION_ID, linkSelf);
				}
			}
			if (!myEntry.getLinkAlternate().isEmpty()) {
				ResourceMetadataKeyEnum.LINK_ALTERNATE.put(myEntry.getResource(), myEntry.getLinkAlternate().getValue());
			}
			if (!myEntry.getLinkSearch().isEmpty()) {
				ResourceMetadataKeyEnum.LINK_SEARCH.put(myEntry.getResource(), myEntry.getLinkSearch().getValue());
			}
			if (!myEntry.getSearchMode().isEmpty()) {
				ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(myEntry.getResource(), myEntry.getSearchMode().getValueAsEnum());
			}
			if (!myEntry.getTransactionMethod().isEmpty()) {
				ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(myEntry.getResource(), myEntry.getTransactionMethod().getValueAsEnum());
			}
			if (!myEntry.getScore().isEmpty()) {
				ResourceMetadataKeyEnum.ENTRY_SCORE.put(myEntry.getResource(), myEntry.getScore());
			}
		}

	}

	private class BundleEntryTransactionState extends BaseState {

		private BundleEntry myEntry;

		public BundleEntryTransactionState(BundleEntry theEntry) {
			super(null);
			myEntry = theEntry;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("method".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getTransactionMethod()));
			} else if ("url".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myEntry.getLinkSearch()));
			} else {
				logAndSwallowUnexpectedElement(theLocalPart);
			}
		}

	}

	private class BundleLinkState extends BaseState {

		private BundleEntry myEntry;
		private String myHref;
		private boolean myInRelation = false;
		private Bundle myInstance;
		private boolean myInUrl = false;
		private String myRel;

		public BundleLinkState(Bundle theInstance) {
			super(null);
			myInstance = theInstance;
		}

		public BundleLinkState(BundleEntry theEntry) {
			super(null);
			myEntry = theEntry;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if (myInRelation) {
				myRel = theValue;
			} else if (myInUrl) {
				myHref = theValue;
			}
		}

		@Override
		public void endingElement() throws DataFormatException {
			if (!myInRelation && !myInUrl) {
				if (myInstance != null) {
					if ("self".equals(myRel)) {
						myInstance.getLinkSelf().setValueAsString(myHref);
					} else if ("first".equals(myRel)) {
						myInstance.getLinkFirst().setValueAsString(myHref);
					} else if ("previous".equals(myRel)) {
						myInstance.getLinkPrevious().setValueAsString(myHref);
					} else if ("next".equals(myRel)) {
						myInstance.getLinkNext().setValueAsString(myHref);
					} else if ("last".equals(myRel)) {
						myInstance.getLinkLast().setValueAsString(myHref);
					} else if ("fhir-base".equals(myRel)) {
						myInstance.getLinkBase().setValueAsString(myHref);
					}
				} else {
					if ("self".equals(myRel)) {
						myEntry.getLinkSelf().setValueAsString(myHref);
					} else if ("search".equals(myRel)) {
						myEntry.getLinkSearch().setValueAsString(myHref);
					} else if ("alternate".equals(myRel)) {
						myEntry.getLinkAlternate().setValueAsString(myHref);
					}
				}
				pop();
			} else {
				myInRelation = false;
				myInUrl = false;
			}
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (myInRelation || myInUrl) {
				throw new DataFormatException("Unexpected element '" + theLocalPart + "' in element 'link'");
			}
			if ("relation".equals(theLocalPart)) {
				myInRelation = true;
			} else if ("url".equals(theLocalPart)) {
				myInUrl = true;
			} else {
				throw new DataFormatException("Unexpected element '" + theLocalPart + "' in element 'link'");
			}
		}

	}

	private class BundleState extends BaseState {

		private Bundle myInstance;
		private Class<? extends IBaseResource> myResourceType;

		public BundleState(Bundle theInstance, Class<? extends IBaseResource> theResourceType) {
			super(null);
			myInstance = theInstance;
			myResourceType = theResourceType;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if ("id".equals(theLocalPart)) {
				push(new PrimitiveState(null, myInstance.getId()));
			} else if ("meta".equals(theLocalPart)) {
				push(new MetaElementState(null, myInstance.getResourceMetadata()));
			} else if ("type".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myInstance.getType()));
			} else if ("base".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myInstance.getLinkBase()));
			} else if ("total".equals(theLocalPart)) {
				push(new PrimitiveState(getPreResourceState(), myInstance.getTotalResults()));
			} else if ("link".equals(theLocalPart)) {
				push(new BundleLinkState(myInstance));
			} else if ("entry".equals(theLocalPart)) {
				push(new BundleEntryState(myInstance, myResourceType));
			} else if ("text".equals(theLocalPart)) {
				push(new SwallowChildrenWholeState(getPreResourceState()));
			} else {
				throw new DataFormatException("Unxpected element '" + theLocalPart + "' in element 'Bundle'");
			}

			// if ("entry".equals(theLocalPart) && verifyNamespace(XmlParser.ATOM_NS, theNamespaceUri)) {
			// push(new AtomEntryState(myInstance, myResourceType));
			// } else if (theLocalPart.equals("published")) {
			// push(new AtomPrimitiveState(myInstance.getPublished()));
			// } else if (theLocalPart.equals("title")) {
			// push(new AtomPrimitiveState(myInstance.getTitle()));
			// } else if ("id".equals(theLocalPart)) {
			// push(new AtomPrimitiveState(myInstance.getBundleId()));
			// } else if ("link".equals(theLocalPart)) {
			// push(new AtomLinkState(myInstance));
			// } else if ("totalResults".equals(theLocalPart) && (verifyNamespace(XmlParser.OPENSEARCH_NS,
			// theNamespaceUri) || verifyNamespace(Constants.OPENSEARCH_NS_OLDER, theNamespaceUri))) {
			// push(new AtomPrimitiveState(myInstance.getTotalResults()));
			// } else if ("updated".equals(theLocalPart)) {
			// push(new AtomPrimitiveState(myInstance.getUpdated()));
			// } else if ("author".equals(theLocalPart)) {
			// push(new AtomAuthorState(myInstance));
			// } else if ("category".equals(theLocalPart)) {
			// push(new AtomCategoryState(myInstance.getCategories()));
			// } else if ("deleted-entry".equals(theLocalPart) && verifyNamespace(XmlParser.TOMBSTONES_NS,
			// theNamespaceUri)) {
			// push(new AtomDeletedEntryState(myInstance, myResourceType));
			// } else {
			// if (theNamespaceUri != null) {
			// throw new DataFormatException("Unexpected element: {" + theNamespaceUri + "}" + theLocalPart);
			// } else {
			// throw new DataFormatException("Unexpected element: " + theLocalPart);
			// }
			// }

			// TODO: handle category and DSig
		}

		@Override
		protected IElement getCurrentElement() {
			return myInstance;
		}

		@Override
		public void wereBack() {
			for (BundleEntry nextEntry : myInstance.getEntries()) {
				IResource nextResource = nextEntry.getResource();
				if (nextResource == null) {
					continue;
				}

				String bundleBaseUrl = myInstance.getLinkBase().getValue();
				//TODO: Use of a deprecated method should be resolved.
				String entryBaseUrl = nextEntry.getLinkBase().getValue();
				String version = ResourceMetadataKeyEnum.VERSION.get(nextResource);
				String resourceName = myContext.getResourceDefinition(nextResource).getName();
				String bundleIdPart = nextResource.getId().getIdPart();
				if (isNotBlank(bundleIdPart)) {
					String baseUrl;
					if (isNotBlank(entryBaseUrl)) {
						baseUrl = entryBaseUrl;
					} else {
						baseUrl = bundleBaseUrl;
					}
					if (baseUrl == null) {
						// nothing
					} else if (!baseUrl.startsWith("cid:") && !baseUrl.startsWith("urn:")) {
						nextResource.setId(new IdDt(baseUrl, resourceName, bundleIdPart, version));
					} else {
						if (baseUrl.endsWith(":")) {
							nextResource.setId(new IdDt(baseUrl + bundleIdPart));
						} else {
							nextResource.setId(new IdDt(baseUrl + ':' + bundleIdPart));
						}
					}
				}
			}

			String bundleVersion = (String) myInstance.getResourceMetadata().get(ResourceMetadataKeyEnum.VERSION);
			String baseUrl = myInstance.getLinkBase().getValue();
			String id = myInstance.getId().getIdPart();
			if (isNotBlank(id)) {
				myInstance.setId(new IdDt(baseUrl, "Bundle", id, bundleVersion));
			}

		}

	}

	private class ContainedResourcesStateHapi extends PreResourceState {

		public ContainedResourcesStateHapi(PreResourceState thePreResourcesState) {
			super(thePreResourcesState, ((IResource) thePreResourcesState.myInstance).getStructureFhirVersionEnum());
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		protected void populateTarget() {
			// nothing
		}

		@Override
		public void wereBack() {
			super.wereBack();

			IResource res = (IResource) getCurrentElement();
			assert res != null;
			if (res.getId() == null || res.getId().isEmpty()) {
				// If there is no ID, we don't keep the resource because it's useless (contained resources
				// need an ID to be referred to)
				myErrorHandler.containedResourceWithNoId(null);
			} else {
				if (!res.getId().isLocal()) {
					res.setId(new IdDt('#' + res.getId().getIdPart()));
				}
				getPreResourceState().getContainedResources().put(res.getId().getValueAsString(), res);
			}
			IResource preResCurrentElement = (IResource) getPreResourceState().getCurrentElement();

			@SuppressWarnings("unchecked")
			List<IResource> containedResources = (List<IResource>) preResCurrentElement.getContained().getContainedResources();
			containedResources.add(res);

		}

	}

	private class ContainedResourcesStateHl7Org extends PreResourceState {

		public ContainedResourcesStateHl7Org(PreResourceState thePreResourcesState) {
			super(thePreResourcesState, thePreResourcesState.myParentVersion);
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		protected void populateTarget() {
			// nothing
		}

		@Override
		public void wereBack() {
			super.wereBack();

			IBaseResource res = getCurrentElement();
			assert res != null;
			if (res.getIdElement() == null || res.getIdElement().isEmpty()) {
				// If there is no ID, we don't keep the resource because it's useless (contained resources
				// need an ID to be referred to)
				myErrorHandler.containedResourceWithNoId(null);
			} else {
				res.getIdElement().setValue('#' + res.getIdElement().getIdPart());
				getPreResourceState().getContainedResources().put(res.getIdElement().getValue(), res);
			}

			IBaseResource preResCurrentElement = getPreResourceState().getCurrentElement();
			RuntimeResourceDefinition def = myContext.getResourceDefinition(preResCurrentElement);
			def.getChildByName("contained").getMutator().addValue(preResCurrentElement, res);
		}

	}

	private class DeclaredExtensionState extends BaseState {

		private IBase myChildInstance;
		private RuntimeChildDeclaredExtensionDefinition myDefinition;
		private IBase myParentInstance;
		private PreResourceState myPreResourceState;

		public DeclaredExtensionState(PreResourceState thePreResourceState, RuntimeChildDeclaredExtensionDefinition theDefinition, IBase theParentInstance) {
			super(thePreResourceState);
			myPreResourceState = thePreResourceState;
			myDefinition = theDefinition;
			myParentInstance = theParentInstance;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if (theName.equals("url")) {
				// This can be ignored
				return;
			}
			super.attributeValue(theName, theValue);
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			BaseRuntimeElementDefinition<?> target = myDefinition.getChildByName(theLocalPart);
			if (target == null) {
				myErrorHandler.unknownElement(null, theLocalPart);
				push(new SwallowChildrenWholeState(getPreResourceState()));
				return;
			}

			switch (target.getChildType()) {
			case COMPOSITE_DATATYPE: {
				BaseRuntimeElementCompositeDefinition<?> compositeTarget = (BaseRuntimeElementCompositeDefinition<?>) target;
				ICompositeType newChildInstance = (ICompositeType) compositeTarget.newInstance(myDefinition.getInstanceConstructorArguments());
				myDefinition.getMutator().addValue(myParentInstance, newChildInstance);
				ElementCompositeState newState = new ElementCompositeState(myPreResourceState, compositeTarget, newChildInstance);
				push(newState);
				return;
			}
			case PRIMITIVE_DATATYPE: {
				RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) target;
				IPrimitiveType<?> newChildInstance = primitiveTarget.newInstance(myDefinition.getInstanceConstructorArguments());
				myDefinition.getMutator().addValue(myParentInstance, newChildInstance);
				PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance);
				push(newState);
				return;
			}
			case PRIMITIVE_XHTML:
			case RESOURCE:
			case RESOURCE_BLOCK:
			case UNDECL_EXT:
			case EXTENSION_DECLARED:
			default:
				break;
			}
		}

		@Override
		public void enteringNewElementExtension(StartElement theElement, String theUrlAttr, boolean theIsModifier) {
			RuntimeChildDeclaredExtensionDefinition declaredExtension = myDefinition.getChildExtensionForUrl(theUrlAttr);
			if (declaredExtension != null) {
				if (myChildInstance == null) {
					myChildInstance = myDefinition.newInstance();
					myDefinition.getMutator().addValue(myParentInstance, myChildInstance);
				}
				BaseState newState = new DeclaredExtensionState(getPreResourceState(), declaredExtension, myChildInstance);
				push(newState);
			} else {
				super.enteringNewElementExtension(theElement, theUrlAttr, theIsModifier);
			}
		}

		@Override
		protected IBase getCurrentElement() {
			return myParentInstance;
		}

	}

	private class ElementCompositeState extends BaseState {

		private BaseRuntimeElementCompositeDefinition<?> myDefinition;
		private IBase myInstance;
		private Set<String> myParsedNonRepeatableNames = new HashSet<String>();

		public ElementCompositeState(PreResourceState thePreResourceState, BaseRuntimeElementCompositeDefinition<?> theDef, IBase theInstance) {
			super(thePreResourceState);
			myDefinition = theDef;
			myInstance = theInstance;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if ("id".equals(theName)) {
				if (myInstance instanceof IIdentifiableElement) {
					((IIdentifiableElement) myInstance).setElementSpecificId((theValue));
				} else if (myInstance instanceof IBaseElement) {
					((IBaseElement) myInstance).setId(theValue);
				} else if (myInstance instanceof IBaseResource) {
					new IdDt(theValue).applyTo((IBaseResource) myInstance);
				}
			} else if ("url".equals(theName) && myInstance instanceof ExtensionDt) {
				((ExtensionDt) myInstance).setUrl(theValue);
			} else {
				super.attributeValue(theName, theValue);
			}
		}

		@Override
		public boolean elementIsRepeating(String theChildName) {
			try {
				BaseRuntimeChildDefinition child = myDefinition.getChildByNameOrThrowDataFormatException(theChildName);
				return child.getMax() > 1 || child.getMax() == Child.MAX_UNLIMITED;
			} catch (DataFormatException e) {
				return false;
			}
		}

		@Override
		public void endingElement() {
			pop();
		}
		
		@Override
		public void enteringNewElement(String theNamespace, String theChildName) throws DataFormatException {
			BaseRuntimeChildDefinition child;
			try {
				child = myDefinition.getChildByNameOrThrowDataFormatException(theChildName);
			} catch (DataFormatException e) {
				if (theChildName.equals("id")) {
					if (getCurrentElement() instanceof IIdentifiableElement) {
						push(new IdentifiableElementIdState(getPreResourceState(), (IIdentifiableElement) getCurrentElement()));
						return;
					}
				}

				/*
				 * This means we've found an element that doesn't exist on the structure. If the error handler doesn't throw
				 * an exception, swallow the element silently along with any child elements
				 */
				myErrorHandler.unknownElement(null, theChildName);
				push(new SwallowChildrenWholeState(getPreResourceState()));
				return;
			}

			if ((child.getMax() == 0 || child.getMax() == 1) && !myParsedNonRepeatableNames.add(theChildName)) {
				myErrorHandler.unexpectedRepeatingElement(null, theChildName);
				push(new SwallowChildrenWholeState(getPreResourceState()));
				return;
			}

			BaseRuntimeElementDefinition<?> target = child.getChildByName(theChildName);
			if (target == null) {
				// This is a bug with the structures and shouldn't happen..
				throw new DataFormatException("Found unexpected element '" + theChildName + "' in parent element '" + myDefinition.getName() + "'. Valid names are: " + child.getValidChildNames());
			}

			switch (target.getChildType()) {
			case COMPOSITE_DATATYPE: {
				BaseRuntimeElementCompositeDefinition<?> compositeTarget = (BaseRuntimeElementCompositeDefinition<?>) target;
				ICompositeType newChildInstance = (ICompositeType) compositeTarget.newInstance(child.getInstanceConstructorArguments());
				child.getMutator().addValue(myInstance, newChildInstance);
				ParserState<T>.ElementCompositeState newState = new ElementCompositeState(getPreResourceState(), compositeTarget, newChildInstance);
				push(newState);
				return;
			}
			case ID_DATATYPE:
			case PRIMITIVE_DATATYPE: {
				RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) target;
				IPrimitiveType<?> newChildInstance;
				newChildInstance = primitiveTarget.newInstance(child.getInstanceConstructorArguments());
				child.getMutator().addValue(myInstance, newChildInstance);
				PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance);
				push(newState);
				return;
			}
			case RESOURCE_BLOCK: {
				RuntimeResourceBlockDefinition blockTarget = (RuntimeResourceBlockDefinition) target;
				IBase newBlockInstance = blockTarget.newInstance();
				child.getMutator().addValue(myInstance, newBlockInstance);
				ElementCompositeState newState = new ElementCompositeState(getPreResourceState(), blockTarget, newBlockInstance);
				push(newState);
				return;
			}
			case PRIMITIVE_XHTML: {
				RuntimePrimitiveDatatypeNarrativeDefinition xhtmlTarget = (RuntimePrimitiveDatatypeNarrativeDefinition) target;
				XhtmlDt newDt = xhtmlTarget.newInstance();
				child.getMutator().addValue(myInstance, newDt);
				XhtmlState state = new XhtmlState(getPreResourceState(), newDt, true);
				push(state);
				return;
			}
			case PRIMITIVE_XHTML_HL7ORG: {
				RuntimePrimitiveDatatypeXhtmlHl7OrgDefinition xhtmlTarget = (RuntimePrimitiveDatatypeXhtmlHl7OrgDefinition) target;
				IBaseXhtml newDt = xhtmlTarget.newInstance();
				child.getMutator().addValue(myInstance, newDt);
				XhtmlStateHl7Org state = new XhtmlStateHl7Org(getPreResourceState(), newDt);
				push(state);
				return;
			}
			case CONTAINED_RESOURCES: {
				List<? extends IBase> values = child.getAccessor().getValues(myInstance);
				Object newDt;
				if (values == null || values.isEmpty() || values.get(0) == null) {
					newDt = newContainedDt((IResource) getPreResourceState().myInstance);
					child.getMutator().addValue(myInstance, (IBase) newDt);
				} else {
					newDt = values.get(0);
				}
				ContainedResourcesStateHapi state = new ContainedResourcesStateHapi(getPreResourceState());
				push(state);
				return;
			}
			case CONTAINED_RESOURCE_LIST: {
				ContainedResourcesStateHl7Org state = new ContainedResourcesStateHl7Org(getPreResourceState());
				push(state);
				return;
			}
			case RESOURCE: {
				if (myInstance instanceof IAnyResource || myInstance instanceof IBaseBackboneElement) {
					ParserState<T>.PreResourceStateHl7Org state = new PreResourceStateHl7Org(myInstance, child.getMutator(), null);
					push(state);
				} else {
					ParserState<T>.PreResourceStateHapi state = new PreResourceStateHapi(myInstance, child.getMutator(), null);
					push(state);
				}
				return;
			}
			case UNDECL_EXT:
			case EXTENSION_DECLARED: {
				// Throw an exception because this shouldn't happen here
				break;
			}
			}

			throw new DataFormatException("Illegal resource position: " + target.getChildType());
		}

		@Override
		public void enteringNewElementExtension(StartElement theElement, String theUrlAttr, boolean theIsModifier) {
			RuntimeChildDeclaredExtensionDefinition declaredExtension = myDefinition.getDeclaredExtension(theUrlAttr);
			if (declaredExtension != null) {
				BaseState newState = new DeclaredExtensionState(getPreResourceState(), declaredExtension, myInstance);
				push(newState);
			} else {
				super.enteringNewElementExtension(theElement, theUrlAttr, theIsModifier);
			}
		}

		@Override
		protected IBase getCurrentElement() {
			return myInstance;
		}

	}

	public class ElementIdState extends BaseState {

		private IBaseElement myElement;

		public ElementIdState(ParserState<T>.PreResourceState thePreResourceState, IBaseElement theElement) {
			super(thePreResourceState);
			myElement = theElement;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			myElement.setId(theValue);
		}

		@Override
		public void endingElement() {
			pop();
		}

	}

	private class ExtensionState extends BaseState {

		private IBaseExtension<?, ?> myExtension;

		public ExtensionState(PreResourceState thePreResourceState, IBaseExtension<?, ?> theExtension) {
			super(thePreResourceState);
			myExtension = theExtension;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if ("url".equals(theName)) {
				// The URL attribute is handles in the XML loop as a special case since it is "url" instead
				// of "value" like every single other place
				return;
			}
			if ("id".equals(theName)) {
				if (getCurrentElement() instanceof IBaseElement) {
					((IBaseElement) getCurrentElement()).setId(theValue);
					return;
				} else if (getCurrentElement() instanceof IIdentifiableElement) {
					((IIdentifiableElement) getCurrentElement()).setElementSpecificId(theValue);
					return;
				}
			}
			super.attributeValue(theName, theValue);
		}

		@Override
		public void endingElement() throws DataFormatException {
			if (myExtension.getValue() != null && myExtension.getExtension().size() > 0) {
				throw new DataFormatException("Extension (URL='" + myExtension.getUrl() + "') must not have both a value and other contained extensions");
			}
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (theLocalPart.equals("id")) {
				if (getCurrentElement() instanceof IBaseElement) {
					push(new ElementIdState(getPreResourceState(), (IBaseElement)getCurrentElement()));
					return;
				} else if (getCurrentElement() instanceof IIdentifiableElement) {
					push(new IdentifiableElementIdState(getPreResourceState(), (IIdentifiableElement)getCurrentElement()));
					return;
				}
			}
			
			BaseRuntimeElementDefinition<?> target = myContext.getRuntimeChildUndeclaredExtensionDefinition().getChildByName(theLocalPart);
			
			if (target != null) {
				switch (target.getChildType()) {
				case COMPOSITE_DATATYPE: {
					BaseRuntimeElementCompositeDefinition<?> compositeTarget = (BaseRuntimeElementCompositeDefinition<?>) target;
					ICompositeType newChildInstance = (ICompositeType) compositeTarget.newInstance();
					myExtension.setValue(newChildInstance);
					ElementCompositeState newState = new ElementCompositeState(getPreResourceState(), compositeTarget, newChildInstance);
					push(newState);
					return;
				}
				case ID_DATATYPE:
				case PRIMITIVE_DATATYPE: {
					RuntimePrimitiveDatatypeDefinition primitiveTarget = (RuntimePrimitiveDatatypeDefinition) target;
					IPrimitiveType<?> newChildInstance = primitiveTarget.newInstance();
					myExtension.setValue(newChildInstance);
					PrimitiveState newState = new PrimitiveState(getPreResourceState(), newChildInstance);
					push(newState);
					return;
				}
				}
			}

			// We hit an invalid type for the extension
			myErrorHandler.unknownElement(null, theLocalPart);
			push(new SwallowChildrenWholeState(getPreResourceState()));
			return;
		}
		
		@Override
		protected IBaseExtension<?, ?> getCurrentElement() {
			return myExtension;
		}

	}

	public class IdentifiableElementIdState extends BaseState {

		private IIdentifiableElement myElement;

		public IdentifiableElementIdState(ParserState<T>.PreResourceState thePreResourceState, IIdentifiableElement theElement) {
			super(thePreResourceState);
			myElement = theElement;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			myElement.setElementSpecificId(theValue);
		}

		@Override
		public void endingElement() {
			pop();
		}

	}

	private class MetaElementState extends BaseState {
		private ResourceMetadataMap myMap;

		public MetaElementState(ParserState<T>.PreResourceState thePreResourceState, ResourceMetadataMap theMap) {
			super(thePreResourceState);
			myMap = theMap;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (theLocalPart.equals("versionId")) {
				push(new MetaVersionElementState(getPreResourceState(), myMap));
				// } else if (theLocalPart.equals("profile")) {
				//
			} else if (theLocalPart.equals("lastUpdated")) {
				InstantDt updated = new InstantDt();
				push(new PrimitiveState(getPreResourceState(), updated));
				myMap.put(ResourceMetadataKeyEnum.UPDATED, updated);
			} else if (theLocalPart.equals("security")) {
				@SuppressWarnings("unchecked")
				List<IBase> securityLabels = (List<IBase>) myMap.get(ResourceMetadataKeyEnum.SECURITY_LABELS);
				if (securityLabels == null) {
					securityLabels = new ArrayList<IBase>();
					myMap.put(ResourceMetadataKeyEnum.SECURITY_LABELS, securityLabels);
				}
				IBase securityLabel = myContext.getVersion().newCodingDt();
				BaseRuntimeElementCompositeDefinition<?> codinfDef = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(securityLabel.getClass());
				push(new SecurityLabelElementStateHapi(getPreResourceState(), codinfDef, securityLabel));
				securityLabels.add(securityLabel);
			} else if (theLocalPart.equals("profile")) {
				@SuppressWarnings("unchecked")
				List<IdDt> profiles = (List<IdDt>) myMap.get(ResourceMetadataKeyEnum.PROFILES);
				List<IdDt> newProfiles;
				if (profiles != null) {
					newProfiles = new ArrayList<IdDt>(profiles.size() + 1);
					newProfiles.addAll(profiles);
				} else {
					newProfiles = new ArrayList<IdDt>(1);
				}
				IdDt profile = new IdDt();
				push(new PrimitiveState(getPreResourceState(), profile));
				newProfiles.add(profile);
				myMap.put(ResourceMetadataKeyEnum.PROFILES, Collections.unmodifiableList(newProfiles));
			} else if (theLocalPart.equals("tag")) {
				TagList tagList = (TagList) myMap.get(ResourceMetadataKeyEnum.TAG_LIST);
				if (tagList == null) {
					tagList = new TagList();
					myMap.put(ResourceMetadataKeyEnum.TAG_LIST, tagList);
				}
				push(new TagState(tagList));
			} else {
				myErrorHandler.unknownElement(null, theLocalPart);
				push(new SwallowChildrenWholeState(getPreResourceState()));
				return;
			}
		}

	}

	private class MetaVersionElementState extends BaseState {

		private ResourceMetadataMap myMap;

		public MetaVersionElementState(ParserState<T>.PreResourceState thePreResourceState, ResourceMetadataMap theMap) {
			super(thePreResourceState);
			myMap = theMap;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			myMap.put(ResourceMetadataKeyEnum.VERSION, theValue);
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			myErrorHandler.unknownElement(null, theLocalPart);
			push(new SwallowChildrenWholeState(getPreResourceState()));
			return;
		}

	}

	private class PreAtomState extends BasePreAtomOrBundleState {

		public PreAtomState(Class<? extends IBaseResource> theResourceType) {
			super(theResourceType);
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (!"feed".equals(theLocalPart)) {
				throw new DataFormatException("Expecting outer element called 'feed', found: " + theLocalPart);
			}

			setInstance(new Bundle());
			push(new AtomState(getInstance(), getResourceType()));

		}

	}

	private class PreBundleState extends BasePreAtomOrBundleState {

		public PreBundleState(Class<? extends IBaseResource> theResourceType) {
			super(theResourceType);
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (!"Bundle".equals(theLocalPart)) {
				throw new DataFormatException("Expecting outer element called 'Bundle', found: " + theLocalPart);
			}

			setInstance(new Bundle());
			push(new BundleState(getInstance(), getResourceType()));

		}

	}

	private abstract class PreResourceState extends BaseState {

		private Map<String, IBaseResource> myContainedResources;
		private IBaseResource myInstance;
		private FhirVersionEnum myParentVersion;
		private boolean myRequireResourceType = true;
		private Class<? extends IBaseResource> myResourceType;

		public PreResourceState(Class<? extends IBaseResource> theResourceType) {
			super(null);
			myResourceType = theResourceType;
			myContainedResources = new HashMap<String, IBaseResource>();
			if (theResourceType != null) {
				myParentVersion = myContext.getResourceDefinition(theResourceType).getStructureVersion();
			} else {
				myParentVersion = myContext.getVersion().getVersion();
			}
		}

		public PreResourceState(PreResourceState thePreResourcesState, FhirVersionEnum theParentVersion) {
			super(thePreResourcesState);
			Validate.notNull(theParentVersion);
			myParentVersion = theParentVersion;
			myContainedResources = thePreResourcesState.getContainedResources();
		}

		@Override
		public void endingElement() throws DataFormatException {
			stitchBundleCrossReferences();
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			BaseRuntimeElementDefinition<?> definition;
			if (myResourceType == null) {
				definition = null;
				if (myParser.getPreferTypes() != null) {
					for (Class<? extends IBaseResource> next : myParser.getPreferTypes()) {
						RuntimeResourceDefinition nextDef = myContext.getResourceDefinition(next);
						if (nextDef.getName().equals(theLocalPart)) {
							definition = nextDef;
						}
					}
				}
				if (definition == null) {
					definition = myContext.getResourceDefinition(myParentVersion, theLocalPart);
				}
				if ((definition == null)) {
					throw new DataFormatException("Element '" + theLocalPart + "' is not a known resource type, expected a resource at this position");
				}
			} else {
				definition = myContext.getResourceDefinition(myResourceType);
				if (!StringUtils.equals(theLocalPart, definition.getName())) {
					if (myRequireResourceType) {
						throw new DataFormatException(myContext.getLocalizer().getMessage(ParserState.class, "wrongResourceTypeFound", definition.getName(), theLocalPart));
					}
					definition = myContext.getResourceDefinition(theLocalPart);
					if (!(definition instanceof RuntimeResourceDefinition)) {
						throw new DataFormatException("Element '" + theLocalPart + "' is not a resource, expected a resource at this position");
					}
				}
			}

			RuntimeResourceDefinition def = (RuntimeResourceDefinition) definition;
			if (!definition.getName().equals(theLocalPart) && definition.getName().equalsIgnoreCase(theLocalPart)) {
				throw new DataFormatException("Unknown resource type '" + theLocalPart + "': Resource names are case sensitive, found similar name: '" + definition.getName() + "'");
			}
			myInstance = def.newInstance();

			String resourceName = def.getName();
			if ("Binary".equals(resourceName) && myContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) {
				push(new BinaryResourceStateForDstu1(getRootPreResourceState(), (IBaseBinary) myInstance));
			} else if (myInstance instanceof IResource) {
				push(new ResourceStateHapi(getRootPreResourceState(), def, (IResource) myInstance));
			} else {
				push(new ResourceStateHl7Org(getRootPreResourceState(), def, myInstance));
			}
		}
		
		public Map<String, IBaseResource> getContainedResources() {
			return myContainedResources;
		}

		@Override
		protected IBaseResource getCurrentElement() {
			return myInstance;
		}

		private PreResourceState getRootPreResourceState() {
			if (getPreResourceState() != null) {
				return getPreResourceState();
			}
			return this;
		}

		@Override
		public boolean isPreResource() {
			return true;
		}

		protected abstract void populateTarget();

		private void postProcess() {
			if (myContext.hasDefaultTypeForProfile()) {
				IBaseMetaType meta = myInstance.getMeta();
				Class<? extends IBaseResource> wantedProfileType = null;
				String usedProfile = null;
				for (IPrimitiveType<String> next : meta.getProfile()) {
					if (isNotBlank(next.getValue())) {
						wantedProfileType = myContext.getDefaultTypeForProfile(next.getValue());
						if (wantedProfileType != null) {
							usedProfile = next.getValue();
							break;
						}
					}
				}

				if (wantedProfileType != null && !wantedProfileType.equals(myInstance.getClass())) {
					if (myResourceType == null || myResourceType.isAssignableFrom(wantedProfileType)) {
						ourLog.debug("Converting resource of type {} to type defined for profile \"{}\": {}", new Object[] { myInstance.getClass().getName(), usedProfile, wantedProfileType });

						/*
						 * This isn't the most efficient thing really.. If we want a specific
						 * type we just re-parse into that type. The problem is that we don't know
						 * until we've parsed the resource which type we want to use because the
						 * profile declarations are in the text of the resource itself.
						 * 
						 * At some point it would be good to write code which can present a view
						 * of one type backed by another type and use that.
						 */
						IParser parser = myContext.newJsonParser();
						String asString = parser.encodeResourceToString(myInstance);
						myInstance = parser.parseResource(wantedProfileType, asString);
					}
				}
			}
			
			populateTarget();
		}

		public ParserState<T>.PreResourceState setRequireResourceType(boolean theRequireResourceType) {
			myRequireResourceType = theRequireResourceType;
			return this;
		}

		private void stitchBundleCrossReferences() {
			final boolean bundle = "Bundle".equals(myContext.getResourceDefinition(myInstance).getName());
			if (bundle) {

				FhirTerser t = myContext.newTerser();
				
				Map<String, IBaseResource> idToResource = new HashMap<String, IBaseResource>();
				if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) {
					List<IBase> entries = t.getValues(myInstance, "Bundle.entry", IBase.class);
					for (IBase nextEntry : entries) {
						IPrimitiveType<?> fullUrl = t.getSingleValueOrNull(nextEntry, "fullUrl", IPrimitiveType.class);
						if (fullUrl != null && isNotBlank(fullUrl.getValueAsString())) {
							IBaseResource resource = t.getSingleValueOrNull(nextEntry, "resource", IBaseResource.class);
							if (resource != null) {
								idToResource.put(fullUrl.getValueAsString(), resource);
							}
						}
					}
				}
				
				/*
				 * Stitch together resource references
				 */
				List<IBaseResource> resources = t.getAllPopulatedChildElementsOfType(myInstance, IBaseResource.class);
				for (IBaseResource next : resources) {
					IIdType id = next.getIdElement();
					if (id != null && id.isEmpty() == false) {
						String resName = myContext.getResourceDefinition(next).getName();
						IIdType idType = id.withResourceType(resName).toUnqualifiedVersionless();
						idToResource.put(idType.getValueAsString(), next);
					}
				}

				for (IBaseResource next : resources) {
					List<IBaseReference> refs = myContext.newTerser().getAllPopulatedChildElementsOfType(next, IBaseReference.class);
					for (IBaseReference nextRef : refs) {
						if (nextRef.isEmpty() == false && nextRef.getReferenceElement() != null) {
							IIdType unqualifiedVersionless = nextRef.getReferenceElement().toUnqualifiedVersionless();
							IBaseResource target = idToResource.get(unqualifiedVersionless.getValueAsString());
							if (target != null) {
								nextRef.setResource(target);
							}
						}
					}
				}

				/*
				 * Set resource IDs based on Bundle.entry.request.url
				 */
				List<Pair<String, IBaseResource>> urlsAndResources = BundleUtil.getBundleEntryUrlsAndResources(myContext, (IBaseBundle) myInstance);
				for (Pair<String, IBaseResource> pair : urlsAndResources) {
					if (pair.getRight() != null && isNotBlank(pair.getLeft()) && pair.getRight().getIdElement().isEmpty()) {
						if (pair.getLeft().startsWith("urn:")) {
							pair.getRight().setId(pair.getLeft());
						}
					}
				}

			}
		}

		protected void weaveContainedResources() {
			FhirTerser terser = myContext.newTerser();
			terser.visit(myInstance, new IModelVisitor() {

				@Override
				public void acceptElement(IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) {
					if (theElement instanceof BaseResourceReferenceDt) {
						BaseResourceReferenceDt nextRef = (BaseResourceReferenceDt) theElement;
						String ref = nextRef.getReference().getValue();
						if (isNotBlank(ref)) {
							if (ref.startsWith("#")) {
								IResource target = (IResource) myContainedResources.get(ref);
								if (target != null) {
									ourLog.debug("Resource contains local ref {} in field {}", ref, thePathToElement);
									nextRef.setResource(target);
								} else {
									myErrorHandler.unknownReference(null, ref);
								}
							}
						}
					} else if (theElement instanceof IBaseReference) {
						IBaseReference nextRef = (IBaseReference) theElement;
						String ref = nextRef.getReferenceElement().getValue();
						if (isNotBlank(ref)) {
							if (ref.startsWith("#")) {
								IBaseResource target = myContainedResources.get(ref);
								if (target != null) {
									ourLog.debug("Resource contains local ref {} in field {}", ref, thePathToElement);
									nextRef.setResource(target);
								} else {
									myErrorHandler.unknownReference(null, ref);
								}
							}
						}
					}
				}
			});
		}

		@Override
		public void wereBack() {
			postProcess();
		}

	}

	private class PreResourceStateHapi extends PreResourceState {
		private BundleEntry myEntry;
		private IMutator myMutator;
		private Object myTarget;

		public PreResourceStateHapi(BundleEntry theEntry, Class<? extends IBaseResource> theResourceType) {
			super(theResourceType);
			myEntry = theEntry;
			assert theResourceType == null || IResource.class.isAssignableFrom(theResourceType);
		}

		public PreResourceStateHapi(Class<? extends IBaseResource> theResourceType) {
			super(theResourceType);
			assert theResourceType == null || IResource.class.isAssignableFrom(theResourceType);
		}

		public PreResourceStateHapi(Object theTarget, IMutator theMutator, Class<? extends IBaseResource> theResourceType) {
			super(theResourceType);
			myTarget = theTarget;
			myMutator = theMutator;
			assert theResourceType == null || IResource.class.isAssignableFrom(theResourceType);
		}

		// @Override
		// public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
		// super.enteringNewElement(theNamespaceUri, theLocalPart);
		// populateTarget();
		// }

		@Override
		protected void populateTarget() {
			weaveContainedResources();
			if (myEntry != null) {
				myEntry.setResource((IResource) getCurrentElement());
			}
			if (myMutator != null) {
				myMutator.setValue(myTarget, getCurrentElement());
			}
		}

		@Override
		public void wereBack() {
			super.wereBack();

			IResource nextResource = (IResource) getCurrentElement();
			String version = ResourceMetadataKeyEnum.VERSION.get(nextResource);
			String resourceName = myContext.getResourceDefinition(nextResource).getName();
			String bundleIdPart = nextResource.getId().getIdPart();
			if (isNotBlank(bundleIdPart)) {
				// if (isNotBlank(entryBaseUrl)) {
				// nextResource.setId(new IdDt(entryBaseUrl, resourceName, bundleIdPart, version));
				// } else {
				nextResource.setId(new IdDt(null, resourceName, bundleIdPart, version));
				// }
			}

		}

	}

	private class PreResourceStateHl7Org extends PreResourceState {

		private IMutator myMutator;
		private Object myTarget;

		public PreResourceStateHl7Org(Class<? extends IBaseResource> theResourceType) {
			super(theResourceType);
		}

		public PreResourceStateHl7Org(Object theTarget, IMutator theMutator, Class<? extends IBaseResource> theResourceType) {
			super(theResourceType);
			myMutator = theMutator;
			myTarget = theTarget;
		}

		@Override
		protected void populateTarget() {
			weaveContainedResources();
			if (myMutator != null) {
				myMutator.setValue(myTarget, getCurrentElement());
			}
		}

		@Override
		public void wereBack() {
			super.wereBack();

			if (getCurrentElement() instanceof IDomainResource) {
				IDomainResource elem = (IDomainResource) getCurrentElement();
				String resourceName = myContext.getResourceDefinition(elem).getName();
				String versionId = elem.getMeta().getVersionId();
				if (StringUtils.isBlank(elem.getIdElement().getIdPart())) {
					// Resource has no ID
				} else if (StringUtils.isNotBlank(versionId)) {
					elem.getIdElement().setValue(resourceName + "/" + elem.getIdElement().getIdPart() + "/_history/" + versionId);
				} else {
					elem.getIdElement().setValue(resourceName + "/" + elem.getIdElement().getIdPart());
				}
			}
		}

	}

	private class PreTagListState extends BaseState {

		private TagList myTagList;

		public PreTagListState() {
			super(null);
			myTagList = new TagList();
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (!TagList.ELEMENT_NAME_LC.equals(theLocalPart.toLowerCase())) {
				throw new DataFormatException("resourceType does not appear to be 'TagList', found: " + theLocalPart);
			}

			push(new TagListState(myTagList));
		}

		@Override
		protected IBase getCurrentElement() {
			return myTagList;
		}

		@Override
		public boolean isPreResource() {
			return true;
		}

	}

	private class PrimitiveState extends BaseState {
		private IPrimitiveType<?> myInstance;

		public PrimitiveState(PreResourceState thePreResourceState, IPrimitiveType<?> theInstance) {
			super(thePreResourceState);
			myInstance = theInstance;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if ("value".equals(theName)) {
				if ("".equals(theValue)) {
					myErrorHandler.invalidValue(null, theValue, "Attribute values must not be empty (\"\")");
				} else {
					try {
						myInstance.setValueAsString(theValue);
					} catch (DataFormatException e) {
						myErrorHandler.invalidValue(null, theValue, e.getMessage());
					} catch (IllegalArgumentException e) {
						myErrorHandler.invalidValue(null, theValue, e.getMessage());
					}
				}
			} else if ("id".equals(theName)) {
				if (myInstance instanceof IIdentifiableElement) {
					((IIdentifiableElement) myInstance).setElementSpecificId(theValue);
				} else if (myInstance instanceof IBaseElement) {
					((IBaseElement) myInstance).setId(theValue);
				} else if (myInstance instanceof IBaseResource) {
					new IdDt(theValue).applyTo((org.hl7.fhir.instance.model.api.IBaseResource) myInstance);
				} else {
					myErrorHandler.unknownAttribute(null, theName);
				}
			} else {
				myErrorHandler.unknownAttribute(null, theName);
			}
		}

		@Override
		public void endingElement() {
			pop();
		}

		// @Override
		// public void enteringNewElementExtension(StartElement theElement,
		// String theUrlAttr) {
		// if (myInstance instanceof ISupportsUndeclaredExtensions) {
		// UndeclaredExtension ext = new UndeclaredExtension(theUrlAttr);
		// ((ISupportsUndeclaredExtensions)
		// myInstance).getUndeclaredExtensions().add(ext);
		// push(new ExtensionState(ext));
		// }
		// }

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			myErrorHandler.unknownElement(null, theLocalPart);
			push(new SwallowChildrenWholeState(getPreResourceState()));
			return;
		}

		@Override
		protected IBase getCurrentElement() {
			return myInstance;
		}

	}

	private class ResourceStateHapi extends ElementCompositeState {

		private IResource myInstance;

		public ResourceStateHapi(PreResourceState thePreResourceState, BaseRuntimeElementCompositeDefinition<?> theDef, IResource theInstance) {
			super(thePreResourceState, theDef, theInstance);
			myInstance = theInstance;
		}

		@Override
		public void enteringNewElement(String theNamespace, String theChildName) throws DataFormatException {
			if ("id".equals(theChildName)) {
				push(new PrimitiveState(getPreResourceState(), myInstance.getId()));
			} else if ("meta".equals(theChildName)) {
				push(new MetaElementState(getPreResourceState(), myInstance.getResourceMetadata()));
			} else {
				super.enteringNewElement(theNamespace, theChildName);
			}
		}
	}

	private class ResourceStateHl7Org extends ElementCompositeState {

		public ResourceStateHl7Org(PreResourceState thePreResourceState, BaseRuntimeElementCompositeDefinition<?> theDef, IBaseResource theInstance) {
			super(thePreResourceState, theDef, theInstance);
		}

	}

	private class SecurityLabelElementStateHapi extends ElementCompositeState {

		public SecurityLabelElementStateHapi(ParserState<T>.PreResourceState thePreResourceState, BaseRuntimeElementCompositeDefinition<?> theDef, IBase codingDt) {
			super(thePreResourceState, theDef, codingDt);
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

	}

	private class SwallowChildrenWholeState extends BaseState {

		private int myDepth;

		public SwallowChildrenWholeState(PreResourceState thePreResourceState) {
			super(thePreResourceState);
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			// ignore
		}

		@Override
		public void endingElement() throws DataFormatException {
			myDepth--;
			if (myDepth < 0) {
				pop();
			}
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			myDepth++;
		}

		@Override
		public void enteringNewElementExtension(StartElement theElement, String theUrlAttr, boolean theIsModifier) {
			myDepth++;
		}

	}

	private class TagListState extends BaseState {

		private TagList myTagList;

		public TagListState(TagList theTagList) {
			super(null);
			myTagList = theTagList;
		}

		@Override
		public void endingElement() throws DataFormatException {
			pop();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			if (TagList.ATTR_CATEGORY.equals(theLocalPart)) {
				push(new TagState(myTagList));
			} else {
				throw new DataFormatException("Unexpected element: " + theLocalPart);
			}
		}

		@Override
		protected IBase getCurrentElement() {
			return myTagList;
		}

	}

	private class TagState extends BaseState {

		private static final int LABEL = 2;
		private static final int NONE = 0;

		private static final int SCHEME = 3;
		private static final int TERM = 1;
		private String myLabel;
		private String myScheme;
		private int mySubState = 0;
		private TagList myTagList;
		private String myTerm;

		public TagState(TagList theTagList) {
			super(null);
			myTagList = theTagList;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			String value = defaultIfBlank(theValue, null);

			switch (mySubState) {
			case TERM:
				myTerm = (value);
				break;
			case LABEL:
				myLabel = (value);
				break;
			case SCHEME:
				myScheme = (value);
				break;
			case NONE:
				// This handles JSON encoding, which is a bit weird
				enteringNewElement(null, theName);
				attributeValue(null, value);
				endingElement();
				break;
			}
		}

		@Override
		public void endingElement() throws DataFormatException {
			if (mySubState != NONE) {
				mySubState = NONE;
			} else {
				if (isNotEmpty(myScheme) || isNotBlank(myTerm) || isNotBlank(myLabel)) {
					myTagList.addTag(myScheme, myTerm, myLabel);
				}
				pop();
			}
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			/*
			 * We allow for both the DSTU1 and DSTU2 names here
			 */
			if (Tag.ATTR_TERM.equals(theLocalPart) || "code".equals(theLocalPart)) {
				mySubState = TERM;
			} else if (Tag.ATTR_SCHEME.equals(theLocalPart) || "system".equals(theLocalPart)) {
				mySubState = SCHEME;
			} else if (Tag.ATTR_LABEL.equals(theLocalPart) || "display".equals(theLocalPart)) {
				mySubState = LABEL;
			} else {
				throw new DataFormatException("Unexpected element: " + theLocalPart);
			}
		}

	}

	private class XhtmlState extends BaseState {
		private int myDepth;
		private XhtmlDt myDt;
		private List<XMLEvent> myEvents = new ArrayList<XMLEvent>();
		private boolean myIncludeOuterEvent;

		private XhtmlState(PreResourceState thePreResourceState, XhtmlDt theXhtmlDt, boolean theIncludeOuterEvent) throws DataFormatException {
			super(thePreResourceState);
			myDepth = 0;
			myDt = theXhtmlDt;
			myIncludeOuterEvent = theIncludeOuterEvent;
		}

		@Override
		public void attributeValue(String theName, String theValue) throws DataFormatException {
			if (myJsonMode) {
				myDt.setValueAsString(theValue);
			} else {				
				// IGNORE - don't handle this as an error, we process these as XML events
			}
		}

		protected void doPop() {
			pop();
		}

		@Override
		public void endingElement() throws DataFormatException {
			if (myJsonMode) {
				doPop();
				return;
			}
			super.endingElement();
		}

		@Override
		public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
			// IGNORE - don't handle this as an error, we process these as XML events
		}

		@Override
		protected IElement getCurrentElement() {
			return myDt;
		}

		public XhtmlDt getDt() {
			return myDt;
		}

		@Override
		public void xmlEvent(XMLEvent theEvent) {
			if (theEvent.isEndElement()) {
				myDepth--;
			}

			if (myIncludeOuterEvent || myDepth > 0) {
				myEvents.add(theEvent);
			}

			if (theEvent.isStartElement()) {
				myDepth++;
			}

			if (theEvent.isEndElement()) {
				if (myDepth == 0) {
					myDt.setValue(myEvents);
					doPop();
				}
			}
		}

	}

	private class XhtmlStateHl7Org extends XhtmlState {
		private IBaseXhtml myHl7OrgDatatype;

		private XhtmlStateHl7Org(PreResourceState thePreResourceState, IBaseXhtml theHl7OrgDatatype) {
			super(thePreResourceState, new XhtmlDt(), true);
			myHl7OrgDatatype = theHl7OrgDatatype;
		}

		@Override
		public void doPop() {
			// TODO: this is not very efficient
			String value = getDt().getValueAsString();
			myHl7OrgDatatype.setValueAsString(value);

			super.doPop();
		}

	}

}
