001package ca.uhn.fhir.context; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2023 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}