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