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