001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 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.ResourceDef;
024import ca.uhn.fhir.util.UrlUtil;
025import org.hl7.fhir.instance.model.api.IAnyResource;
026import org.hl7.fhir.instance.model.api.IBase;
027import org.hl7.fhir.instance.model.api.IBaseResource;
028import org.hl7.fhir.instance.model.api.IDomainResource;
029
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.Comparator;
033import java.util.HashMap;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037
038public class RuntimeResourceDefinition extends BaseRuntimeElementCompositeDefinition<IBaseResource> {
039
040        private Class<? extends IBaseResource> myBaseType;
041        private Map<String, List<RuntimeSearchParam>> myCompartmentNameToSearchParams;
042        private FhirContext myContext;
043        private String myId;
044        private Map<String, RuntimeSearchParam> myNameToSearchParam = new LinkedHashMap<String, RuntimeSearchParam>();
045        private IBaseResource myProfileDef;
046        private String myResourceProfile;
047        private List<RuntimeSearchParam> mySearchParams;
048        private final FhirVersionEnum myStructureVersion;
049        private volatile RuntimeResourceDefinition myBaseDefinition;
050
051
052
053        public RuntimeResourceDefinition(FhirContext theContext, String theResourceName, Class<? extends IBaseResource> theClass, ResourceDef theResourceAnnotation, boolean theStandardType, Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theClassToElementDefinitions) {
054                super(theResourceName, theClass, theStandardType, theContext, theClassToElementDefinitions);
055                myContext = theContext;
056                myResourceProfile = theResourceAnnotation.profile();
057                myId = theResourceAnnotation.id();
058
059                IBaseResource instance;
060                try {
061                        instance = theClass.getConstructor().newInstance();
062                } catch (Exception e) {
063                        throw new ConfigurationException(Msg.code(1730) + myContext.getLocalizer().getMessage(getClass(), "nonInstantiableType", theClass.getName(), e.toString()), e);
064                }
065                myStructureVersion = instance.getStructureFhirVersionEnum();
066                if (myStructureVersion != theContext.getVersion().getVersion()) {
067                        if (myStructureVersion == FhirVersionEnum.R5 && theContext.getVersion().getVersion() == FhirVersionEnum.R4B) {
068                                // TODO: remove this exception once we've bumped FHIR core to a new version
069                                // TODO: also fix the TODO in ModelScanner
070                                // TODO: also fix the TODO in RestfulServerUtils
071                                // TODO: also fix the TODO in BaseParser
072                        } else {
073                                throw new ConfigurationException(Msg.code(1731) + myContext.getLocalizer().getMessage(getClass(), "typeWrongVersion", theContext.getVersion().getVersion(), theClass.getName(), myStructureVersion));
074                        }
075                }
076
077        }
078
079
080        public void addSearchParam(RuntimeSearchParam theParam) {
081                myNameToSearchParam.put(theParam.getName(), theParam);
082        }
083
084        /**
085         * If this definition refers to a class which extends another resource definition type, this
086         * method will return the definition of the topmost resource. For example, if this definition
087         * refers to MyPatient2, which extends MyPatient, which in turn extends Patient, this method
088         * will return the resource definition for Patient.
089         * <p>
090         * If the definition has no parent, returns <code>this</code>
091         * </p>
092         */
093        public RuntimeResourceDefinition getBaseDefinition() {
094                validateSealed();
095                if (myBaseDefinition == null) {
096                        myBaseDefinition = myContext.getResourceDefinition(myBaseType);
097                }
098                return myBaseDefinition;
099        }
100
101        @Override
102        public ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum getChildType() {
103                return ChildTypeEnum.RESOURCE;
104        }
105
106        public String getId() {
107                return myId;
108        }
109
110        /**
111         * Express {@link #getImplementingClass()} as theClass (to prevent casting warnings)
112         */
113        @SuppressWarnings("unchecked")
114        public <T> Class<T> getImplementingClass(Class<T> theClass) {
115                if (!theClass.isAssignableFrom(getImplementingClass())) {
116                        throw new ConfigurationException(Msg.code(1732) + "Unable to convert " + getImplementingClass() + " to " + theClass);
117                }
118                return (Class<T>) getImplementingClass();
119        }
120
121        @Deprecated
122        public String getResourceProfile() {
123                return myResourceProfile;
124        }
125
126        public String getResourceProfile(String theServerBase) {
127                validateSealed();
128                String profile;
129                if (!myResourceProfile.isEmpty()) {
130                        profile = myResourceProfile;
131                } else if (!myId.isEmpty()) {
132                        profile = myId;
133                } else {
134                        return "";
135                }
136
137                if (!UrlUtil.isValid(profile)) {
138                        String resourceName = "/StructureDefinition/";
139                        String profileWithUrl = theServerBase + resourceName + profile;
140                        if (UrlUtil.isValid(profileWithUrl)) {
141                                return profileWithUrl;
142                        }
143                }
144                return profile;
145        }
146
147        public RuntimeSearchParam getSearchParam(String theName) {
148                validateSealed();
149                return myNameToSearchParam.get(theName);
150        }
151
152        public List<RuntimeSearchParam> getSearchParams() {
153                validateSealed();
154                return mySearchParams;
155        }
156
157        /**
158         * Will not return null
159         */
160        public List<RuntimeSearchParam> getSearchParamsForCompartmentName(String theCompartmentName) {
161                validateSealed();
162                List<RuntimeSearchParam> retVal = myCompartmentNameToSearchParams.get(theCompartmentName);
163                if (retVal == null) {
164                        return Collections.emptyList();
165                }
166                return retVal;
167        }
168
169        public FhirVersionEnum getStructureVersion() {
170                return myStructureVersion;
171        }
172
173        public boolean isBundle() {
174                return "Bundle".equals(getName());
175        }
176
177        @SuppressWarnings("unchecked")
178        @Override
179        public void sealAndInitialize(FhirContext theContext, Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theClassToElementDefinitions) {
180                super.sealAndInitialize(theContext, theClassToElementDefinitions);
181
182                myNameToSearchParam = Collections.unmodifiableMap(myNameToSearchParam);
183
184                ArrayList<RuntimeSearchParam> searchParams = new ArrayList<RuntimeSearchParam>(myNameToSearchParam.values());
185                Collections.sort(searchParams, new Comparator<RuntimeSearchParam>() {
186                        @Override
187                        public int compare(RuntimeSearchParam theArg0, RuntimeSearchParam theArg1) {
188                                return theArg0.getName().compareTo(theArg1.getName());
189                        }
190                });
191                mySearchParams = Collections.unmodifiableList(searchParams);
192
193                Map<String, List<RuntimeSearchParam>> compartmentNameToSearchParams = new HashMap<>();
194                for (RuntimeSearchParam next : searchParams) {
195                        if (next.getProvidesMembershipInCompartments() != null) {
196                                for (String nextCompartment : next.getProvidesMembershipInCompartments()) {
197
198                                        if (nextCompartment.startsWith("Base FHIR compartment definition for ")) {
199                                                nextCompartment = nextCompartment.substring("Base FHIR compartment definition for ".length());
200                                        }
201
202                                        if (!compartmentNameToSearchParams.containsKey(nextCompartment)) {
203                                                compartmentNameToSearchParams.put(nextCompartment, new ArrayList<>());
204                                        }
205                                        List<RuntimeSearchParam> searchParamsForCompartment = compartmentNameToSearchParams.get(nextCompartment);
206                                        searchParamsForCompartment.add(next);
207
208                                        /*
209                                         * If one search parameter marks an SP as making a resource
210                                         * a part of a compartment, let's also denote all other
211                                         * SPs with the same path the same way. This behaviour is
212                                         * used by AuthorizationInterceptor
213                                         */
214                                        String nextPath = massagePathForCompartmentSimilarity(next.getPath());
215                                        for (RuntimeSearchParam nextAlternate : searchParams) {
216                                                String nextAlternatePath = massagePathForCompartmentSimilarity(nextAlternate.getPath());
217                                                if (nextAlternatePath.equals(nextPath)) {
218                                                        if (!nextAlternate.getName().equals(next.getName())) {
219                                                                searchParamsForCompartment.add(nextAlternate);
220                                                        }
221                                                }
222                                        }
223                                }
224                        }
225                }
226
227                // Make the map of lists completely unmodifiable
228                for (String nextKey : new ArrayList<>(compartmentNameToSearchParams.keySet())) {
229                        List<RuntimeSearchParam> nextList = compartmentNameToSearchParams.get(nextKey);
230                        compartmentNameToSearchParams.put(nextKey, Collections.unmodifiableList(nextList));
231                }
232                myCompartmentNameToSearchParams = Collections.unmodifiableMap(compartmentNameToSearchParams);
233
234                Class<?> target = getImplementingClass();
235                myBaseType = (Class<? extends IBaseResource>) target;
236                do {
237                        target = target.getSuperclass();
238                        if (IBaseResource.class.isAssignableFrom(target) && target.getAnnotation(ResourceDef.class) != null) {
239                                myBaseType = (Class<? extends IBaseResource>) target;
240                        }
241                } while (target.equals(Object.class) == false);
242                
243                /*
244                 * See #504:
245                 * Bundle types may not have extensions
246                 */
247                if (hasExtensions()) {
248                        if (IAnyResource.class.isAssignableFrom(getImplementingClass())) {
249                                if (!IDomainResource.class.isAssignableFrom(getImplementingClass())) {
250                                        throw new ConfigurationException(Msg.code(1733) + "Class \"" + getImplementingClass() + "\" is invalid. This resource type is not a DomainResource, it must not have extensions");
251                                }
252                        }
253                }
254
255        }
256
257        private String massagePathForCompartmentSimilarity(String thePath) {
258                String path = thePath;
259                if (path.matches(".*\\.where\\(resolve\\(\\) is [a-zA-Z]+\\)")) {
260                        path = path.substring(0, path.indexOf(".where"));
261                }
262                return path;
263        }
264
265        @Deprecated
266        public synchronized IBaseResource toProfile() {
267                validateSealed();
268                if (myProfileDef != null) {
269                        return myProfileDef;
270                }
271
272                IBaseResource retVal = myContext.getVersion().generateProfile(this, null);
273                myProfileDef = retVal;
274
275                return retVal;
276        }
277
278        public synchronized IBaseResource toProfile(String theServerBase) {
279                validateSealed();
280                if (myProfileDef != null) {
281                        return myProfileDef;
282                }
283
284                IBaseResource retVal = myContext.getVersion().generateProfile(this, theServerBase);
285                myProfileDef = retVal;
286
287                return retVal;
288        }
289
290}