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