001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.context;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.model.api.annotation.Child;
024import ca.uhn.fhir.model.api.annotation.Description;
025import jakarta.annotation.Nonnull;
026import org.apache.commons.lang3.StringUtils;
027import org.apache.commons.lang3.Validate;
028import org.hl7.fhir.instance.model.api.IBase;
029import org.hl7.fhir.instance.model.api.IBaseDatatype;
030import org.hl7.fhir.instance.model.api.IBaseReference;
031import org.hl7.fhir.instance.model.api.IBaseResource;
032import org.hl7.fhir.instance.model.api.IPrimitiveType;
033
034import java.lang.reflect.Field;
035import java.util.ArrayList;
036import java.util.Collections;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041
042public class RuntimeChildChoiceDefinition extends BaseRuntimeDeclaredChildDefinition {
043
044        private List<Class<? extends IBase>> myChoiceTypes;
045        private Map<String, BaseRuntimeElementDefinition<?>> myNameToChildDefinition;
046        private Map<Class<? extends IBase>, String> myDatatypeToElementName;
047        private Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myDatatypeToElementDefinition;
048        private String myReferenceSuffix;
049        private List<Class<? extends IBaseResource>> myResourceTypes;
050        private List<Class<? extends IBase>> mySpecializationChoiceTypes = Collections.emptyList();
051
052        /**
053         * Constructor
054         */
055        public RuntimeChildChoiceDefinition(
056                        Field theField,
057                        String theElementName,
058                        Child theChildAnnotation,
059                        Description theDescriptionAnnotation,
060                        List<Class<? extends IBase>> theChoiceTypes) {
061                super(theField, theChildAnnotation, theDescriptionAnnotation, theElementName);
062
063                myChoiceTypes = Collections.unmodifiableList(theChoiceTypes);
064        }
065
066        /**
067         * Constructor
068         *
069         * For extension, if myChoiceTypes will be set some other way
070         */
071        RuntimeChildChoiceDefinition(
072                        Field theField, String theElementName, Child theChildAnnotation, Description theDescriptionAnnotation) {
073                super(theField, theChildAnnotation, theDescriptionAnnotation, theElementName);
074        }
075
076        void setChoiceTypes(
077                        @Nonnull List<Class<? extends IBase>> theChoiceTypes,
078                        @Nonnull List<Class<? extends IBase>> theSpecializationChoiceTypes) {
079                Validate.notNull(theChoiceTypes, "theChoiceTypes must not be null");
080                Validate.notNull(theSpecializationChoiceTypes, "theSpecializationChoiceTypes must not be null");
081                myChoiceTypes = Collections.unmodifiableList(theChoiceTypes);
082                mySpecializationChoiceTypes = Collections.unmodifiableList(theSpecializationChoiceTypes);
083        }
084
085        public List<Class<? extends IBase>> getChoices() {
086                return myChoiceTypes;
087        }
088
089        @Override
090        public Set<String> getValidChildNames() {
091                return myNameToChildDefinition.keySet();
092        }
093
094        @Override
095        public BaseRuntimeElementDefinition<?> getChildByName(String theName) {
096                assert myNameToChildDefinition.containsKey(theName)
097                                : "Can't find child '" + theName + "' in names: " + myNameToChildDefinition.keySet();
098
099                return myNameToChildDefinition.get(theName);
100        }
101
102        @SuppressWarnings("unchecked")
103        @Override
104        void sealAndInitialize(
105                        FhirContext theContext,
106                        Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theClassToElementDefinitions) {
107                myNameToChildDefinition = new HashMap<>();
108                myDatatypeToElementName = new HashMap<>();
109                myDatatypeToElementDefinition = new HashMap<>();
110                myResourceTypes = new ArrayList<>();
111
112                myReferenceSuffix = "Reference";
113
114                sealAndInitializeChoiceTypes(theContext, theClassToElementDefinitions, mySpecializationChoiceTypes, true);
115                sealAndInitializeChoiceTypes(theContext, theClassToElementDefinitions, myChoiceTypes, false);
116
117                myNameToChildDefinition = Collections.unmodifiableMap(myNameToChildDefinition);
118                myDatatypeToElementName = Collections.unmodifiableMap(myDatatypeToElementName);
119                myDatatypeToElementDefinition = Collections.unmodifiableMap(myDatatypeToElementDefinition);
120                myResourceTypes = Collections.unmodifiableList(myResourceTypes);
121        }
122
123        private void sealAndInitializeChoiceTypes(
124                        FhirContext theContext,
125                        Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theClassToElementDefinitions,
126                        List<Class<? extends IBase>> choiceTypes,
127                        boolean theIsSpecilization) {
128                for (Class<? extends IBase> next : choiceTypes) {
129
130                        String elementName = null;
131                        BaseRuntimeElementDefinition<?> nextDef;
132                        boolean nonPreferred = false;
133                        if (IBaseResource.class.isAssignableFrom(next)) {
134                                elementName = getElementName() + StringUtils.capitalize(next.getSimpleName());
135                                nextDef = findResourceReferenceDefinition(theClassToElementDefinitions);
136
137                                if (!theIsSpecilization) {
138                                        myNameToChildDefinition.put(getElementName() + "Reference", nextDef);
139                                        myNameToChildDefinition.put(getElementName() + "Resource", nextDef);
140                                }
141
142                                myResourceTypes.add((Class<? extends IBaseResource>) next);
143
144                        } else {
145                                nextDef = theClassToElementDefinitions.get(next);
146                                BaseRuntimeElementDefinition<?> nextDefForChoice = nextDef;
147
148                                /*
149                                 * In HAPI 1.3 the following applied:
150                                 * Elements which are called foo[x] and have a choice which is a profiled datatype must use the
151                                 * unprofiled datatype as the element name. E.g. if foo[x] allows markdown as a datatype, it calls the
152                                 * element fooString when encoded, because markdown is a profile of string. This is according to the
153                                 * FHIR spec
154                                 *
155                                 * Note that as of HAPI 1.4 this applies only to non-primitive datatypes after discussion
156                                 * with Grahame.
157                                 */
158                                if (nextDef instanceof IRuntimeDatatypeDefinition) {
159                                        IRuntimeDatatypeDefinition nextDefDatatype = (IRuntimeDatatypeDefinition) nextDef;
160                                        if (nextDefDatatype.getProfileOf() != null && !IPrimitiveType.class.isAssignableFrom(next)) {
161                                                nextDefForChoice = null;
162                                                nonPreferred = true;
163                                                Class<? extends IBaseDatatype> profileType = nextDefDatatype.getProfileOf();
164                                                BaseRuntimeElementDefinition<?> elementDef = theClassToElementDefinitions.get(profileType);
165                                                elementName = getElementName() + StringUtils.capitalize(elementDef.getName());
166                                        }
167                                }
168                                if (nextDefForChoice != null) {
169                                        elementName = getElementName() + StringUtils.capitalize(nextDefForChoice.getName());
170                                }
171                        }
172
173                        // I don't see how elementName could be null here, but eclipse complains..
174                        if (!theIsSpecilization) {
175                                if (elementName != null) {
176                                        if (!myNameToChildDefinition.containsKey(elementName) || !nonPreferred) {
177                                                myNameToChildDefinition.put(elementName, nextDef);
178                                        }
179                                }
180
181                                /*
182                                 * If this is a resource reference, the element name is "fooNameReference"
183                                 */
184                                if (IBaseResource.class.isAssignableFrom(next) || IBaseReference.class.isAssignableFrom(next)) {
185                                        next = theContext.getVersion().getResourceReferenceType();
186                                        elementName = getElementName() + myReferenceSuffix;
187                                        myNameToChildDefinition.put(elementName, nextDef);
188                                }
189                        }
190
191                        myDatatypeToElementDefinition.put(next, nextDef);
192
193                        if (myDatatypeToElementName.containsKey(next)) {
194                                String existing = myDatatypeToElementName.get(next);
195                                if (!existing.equals(elementName)) {
196                                        throw new ConfigurationException(
197                                                        Msg.code(1693) + "Already have element name " + existing + " for datatype "
198                                                                        + next.getSimpleName() + " in " + getElementName() + ", cannot add " + elementName);
199                                }
200                        } else {
201                                myDatatypeToElementName.put(next, elementName);
202                        }
203                }
204        }
205
206        public List<Class<? extends IBaseResource>> getResourceTypes() {
207                return myResourceTypes;
208        }
209
210        @Override
211        public String getChildNameByDatatype(Class<? extends IBase> theDatatype) {
212                return myDatatypeToElementName.get(theDatatype);
213        }
214
215        @Override
216        public BaseRuntimeElementDefinition<?> getChildElementDefinitionByDatatype(Class<? extends IBase> theDatatype) {
217                return myDatatypeToElementDefinition.get(theDatatype);
218        }
219
220        public Set<Class<? extends IBase>> getValidChildTypes() {
221                return Collections.unmodifiableSet((myDatatypeToElementDefinition.keySet()));
222        }
223}