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}