001package ca.uhn.fhir.jpa.dao; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 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.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.RuntimeSearchParam; 027import ca.uhn.fhir.interceptor.api.HookParams; 028import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 029import ca.uhn.fhir.interceptor.api.Pointcut; 030import ca.uhn.fhir.interceptor.model.RequestPartitionId; 031import ca.uhn.fhir.jpa.api.config.DaoConfig; 032import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 033import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 034import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome; 035import ca.uhn.fhir.jpa.cache.IResourceVersionSvc; 036import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap; 037import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 038import ca.uhn.fhir.jpa.model.entity.ModelConfig; 039import ca.uhn.fhir.jpa.model.entity.ResourceTable; 040import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 041import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 042import ca.uhn.fhir.model.api.IQueryParameterAnd; 043import ca.uhn.fhir.rest.api.QualifiedParamList; 044import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 045import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 046import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 047import ca.uhn.fhir.rest.api.server.RequestDetails; 048import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; 049import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 050import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; 051import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 052import ca.uhn.fhir.rest.param.QualifierDetails; 053import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 054import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 055import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 056import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 057import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 058import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; 059import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 060import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 061import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 062import ca.uhn.fhir.util.BundleUtil; 063import ca.uhn.fhir.util.FhirTerser; 064import ca.uhn.fhir.util.OperationOutcomeUtil; 065import ca.uhn.fhir.util.ResourceReferenceInfo; 066import com.google.common.annotations.VisibleForTesting; 067import org.hl7.fhir.instance.model.api.IBaseBundle; 068import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 069import org.hl7.fhir.instance.model.api.IBaseReference; 070import org.hl7.fhir.instance.model.api.IBaseResource; 071import org.hl7.fhir.instance.model.api.IIdType; 072import org.hl7.fhir.instance.model.api.IPrimitiveType; 073import org.hl7.fhir.r4.model.InstantType; 074import org.springframework.beans.factory.annotation.Autowired; 075import org.springframework.transaction.annotation.Propagation; 076import org.springframework.transaction.annotation.Transactional; 077 078import javax.annotation.Nonnull; 079import javax.annotation.Nullable; 080import java.util.Collection; 081import java.util.Collections; 082import java.util.IdentityHashMap; 083import java.util.List; 084import java.util.Map; 085import java.util.Set; 086import java.util.function.Supplier; 087 088import static org.apache.commons.lang3.StringUtils.defaultString; 089import static org.apache.commons.lang3.StringUtils.isNotBlank; 090 091public abstract class BaseStorageDao { 092 public static final String OO_SEVERITY_ERROR = "error"; 093 public static final String OO_SEVERITY_INFO = "information"; 094 public static final String OO_SEVERITY_WARN = "warning"; 095 private static final String PROCESSING_SUB_REQUEST = "BaseStorageDao.processingSubRequest"; 096 097 @Autowired 098 protected ISearchParamRegistry mySearchParamRegistry; 099 @Autowired 100 protected FhirContext myFhirContext; 101 @Autowired 102 protected DaoRegistry myDaoRegistry; 103 @Autowired 104 protected ModelConfig myModelConfig; 105 @Autowired 106 protected IResourceVersionSvc myResourceVersionSvc; 107 @Autowired 108 protected DaoConfig myDaoConfig; 109 110 /** 111 * @see ModelConfig#getAutoVersionReferenceAtPaths() 112 */ 113 @Nonnull 114 public static Set<IBaseReference> extractReferencesToAutoVersion(FhirContext theFhirContext, ModelConfig theModelConfig, IBaseResource theResource) { 115 Map<IBaseReference, Object> references = Collections.emptyMap(); 116 if (!theModelConfig.getAutoVersionReferenceAtPaths().isEmpty()) { 117 String resourceName = theFhirContext.getResourceType(theResource); 118 for (String nextPath : theModelConfig.getAutoVersionReferenceAtPathsByResourceType(resourceName)) { 119 List<IBaseReference> nextReferences = theFhirContext.newTerser().getValues(theResource, nextPath, IBaseReference.class); 120 for (IBaseReference next : nextReferences) { 121 if (next.getReferenceElement().hasVersionIdPart()) { 122 continue; 123 } 124 if (references.isEmpty()) { 125 references = new IdentityHashMap<>(); 126 } 127 references.put(next, null); 128 } 129 } 130 } 131 return references.keySet(); 132 } 133 134 public static void clearRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 135 if (theRequestDetails != null) { 136 theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); 137 } 138 } 139 140 public static void markRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 141 if (theRequestDetails != null) { 142 theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); 143 } 144 } 145 146 @VisibleForTesting 147 public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { 148 mySearchParamRegistry = theSearchParamRegistry; 149 } 150 151 /** 152 * May be overridden by subclasses to validate resources prior to storage 153 * 154 * @param theResource The resource that is about to be stored 155 * @deprecated Use {@link #preProcessResourceForStorage(IBaseResource, RequestDetails, TransactionDetails, boolean)} instead 156 */ 157 protected void preProcessResourceForStorage(IBaseResource theResource) { 158 // nothing 159 } 160 161 /** 162 * May be overridden by subclasses to validate resources prior to storage 163 * 164 * @param theResource The resource that is about to be stored 165 * @since 5.3.0 166 */ 167 protected void preProcessResourceForStorage(IBaseResource theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean thePerformIndexing) { 168 169 verifyResourceTypeIsAppropriateForDao(theResource); 170 171 verifyResourceIdIsValid(theResource); 172 173 verifyBundleTypeIsAppropriateForStorage(theResource); 174 175 if(!getConfig().getTreatBaseUrlsAsLocal().isEmpty()) { 176 FhirTerser terser = myFhirContext.newTerser(); 177 replaceAbsoluteReferencesWithRelative(theResource, terser); 178 replaceAbsoluteUrisWithRelative(theResource, terser); 179 } 180 181 performAutoVersioning(theResource, thePerformIndexing); 182 183 } 184 185 /** 186 * Sanity check - Is this resource the right type for this DAO? 187 */ 188 private void verifyResourceTypeIsAppropriateForDao(IBaseResource theResource) { 189 String type = getContext().getResourceType(theResource); 190 if (getResourceName() != null && !getResourceName().equals(type)) { 191 throw new InvalidRequestException(Msg.code(520) + getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "incorrectResourceType", type, getResourceName())); 192 } 193 } 194 195 /** 196 * Verify that the resource ID is actually valid according to FHIR's rules 197 */ 198 private void verifyResourceIdIsValid(IBaseResource theResource) { 199 if (theResource.getIdElement().hasIdPart()) { 200 if (!theResource.getIdElement().isIdPartValid()) { 201 throw new InvalidRequestException(Msg.code(521) + getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart())); 202 } 203 } 204 } 205 206 /** 207 * Verify that we're not storing a Bundle with a disallowed bundle type 208 */ 209 private void verifyBundleTypeIsAppropriateForStorage(IBaseResource theResource) { 210 if (theResource instanceof IBaseBundle) { 211 Set<String> allowedBundleTypes = getConfig().getBundleTypesAllowedForStorage(); 212 String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource); 213 bundleType = defaultString(bundleType); 214 if (!allowedBundleTypes.contains(bundleType)) { 215 String message = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidBundleTypeForStorage", (isNotBlank(bundleType) ? bundleType : "(missing)")); 216 throw new UnprocessableEntityException(Msg.code(522) + message); 217 } 218 } 219 } 220 221 /** 222 * Replace absolute references with relative ones if configured to do so 223 */ 224 private void replaceAbsoluteReferencesWithRelative(IBaseResource theResource, FhirTerser theTerser) { 225 List<ResourceReferenceInfo> refs = theTerser.getAllResourceReferences(theResource); 226 for (ResourceReferenceInfo nextRef : refs) { 227 IIdType refId = nextRef.getResourceReference().getReferenceElement(); 228 if (refId != null && refId.hasBaseUrl()) { 229 if (getConfig().getTreatBaseUrlsAsLocal().contains(refId.getBaseUrl())) { 230 IIdType newRefId = refId.toUnqualified(); 231 nextRef.getResourceReference().setReference(newRefId.getValue()); 232 } 233 } 234 } 235 } 236 237 /** 238 * Replace Canonical URI's with local references, if we find that the canonical should be treated as local. 239 */ 240 private void replaceAbsoluteUrisWithRelative(IBaseResource theResource, FhirTerser theTerser) { 241 242 BaseRuntimeElementDefinition<?> canonicalElementDefinition = myFhirContext.getElementDefinition("canonical"); 243 if (canonicalElementDefinition != null) { 244 Class<? extends IPrimitiveType<String>> canonicalType = (Class<? extends IPrimitiveType<String>>) canonicalElementDefinition.getImplementingClass(); 245 List<? extends IPrimitiveType<String>> canonicals = theTerser.getAllPopulatedChildElementsOfType(theResource, canonicalType); 246 247 //TODO GGG this is pretty inefficient if there are many baseUrls, and many canonicals. Consider improving. 248 for (String baseUrl : myModelConfig.getTreatBaseUrlsAsLocal()) { 249 for (IPrimitiveType<String> canonical : canonicals) { 250 if (canonical.getValue().startsWith(baseUrl)) { 251 canonical.setValue(canonical.getValue().substring(baseUrl.length() + 1)); 252 } 253 } 254 } 255 } 256 } 257 258 /** 259 * Handle {@link ModelConfig#getAutoVersionReferenceAtPaths() auto-populate-versions} 260 * <p> 261 * We only do this if thePerformIndexing is true because if it's false, that means 262 * we're in a FHIR transaction during the first phase of write operation processing, 263 * meaning that the versions of other resources may not have need updatd yet. For example 264 * we're about to store an Observation with a reference to a Patient, and that Patient 265 * is also being updated in the same transaction, during the first "no index" phase, 266 * the Patient will not yet have its version number incremented, so it would be wrong 267 * to use that value. During the second phase it is correct. 268 * <p> 269 * Also note that {@link BaseTransactionProcessor} also has code to do auto-versioning 270 * and it is the one that takes care of the placeholder IDs. Look for the other caller of 271 * {@link #extractReferencesToAutoVersion(FhirContext, ModelConfig, IBaseResource)} 272 * to find this. 273 */ 274 private void performAutoVersioning(IBaseResource theResource, boolean thePerformIndexing) { 275 if (thePerformIndexing) { 276 Set<IBaseReference> referencesToVersion = extractReferencesToAutoVersion(myFhirContext, myModelConfig, theResource); 277 for (IBaseReference nextReference : referencesToVersion) { 278 IIdType referenceElement = nextReference.getReferenceElement(); 279 if (!referenceElement.hasBaseUrl()) { 280 281 ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(), 282 Collections.singletonList(referenceElement) 283 ); 284 285 // 3 cases: 286 // 1) there exists a resource in the db with some version (use this version) 287 // 2) no resource exists, but we will create one (eventually). The version is 1 288 // 3) no resource exists, and none will be made -> throw 289 Long version; 290 if (resourceVersionMap.containsKey(referenceElement)) { 291 // the resource exists... latest id 292 // will be the value in the ResourcePersistentId 293 version = resourceVersionMap.getResourcePersistentId(referenceElement).getVersion(); 294 } else if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) { 295 // if idToPID doesn't contain object 296 // but autcreateplaceholders is on 297 // then the version will be 1 (the first version) 298 version = 1L; 299 } else { 300 // resource not found 301 // and no autocreateplaceholders set... 302 // we throw 303 throw new ResourceNotFoundException(Msg.code(523) + referenceElement); 304 } 305 String newTargetReference = referenceElement.withVersion(version.toString()).getValue(); 306 nextReference.setReference(newTargetReference); 307 } 308 } 309 } 310 } 311 312 protected DaoMethodOutcome toMethodOutcome(RequestDetails theRequest, @Nonnull final IBasePersistedResource theEntity, @Nonnull IBaseResource theResource) { 313 DaoMethodOutcome outcome = new DaoMethodOutcome().setPersistentId(theEntity.getPersistentId()); 314 315 if (theEntity instanceof ResourceTable) { 316 if (((ResourceTable) theEntity).isUnchangedInCurrentOperation()) { 317 outcome.setNop(true); 318 } 319 } 320 321 IIdType id = null; 322 if (theResource.getIdElement().getValue() != null) { 323 id = theResource.getIdElement(); 324 } 325 if (id == null) { 326 id = theEntity.getIdDt(); 327 if (getContext().getVersion().getVersion().isRi()) { 328 id = getContext().getVersion().newIdType().setValue(id.getValue()); 329 } 330 } 331 332 outcome.setId(id); 333 if (theEntity.getDeleted() == null) { 334 outcome.setResource(theResource); 335 } 336 outcome.setEntity(theEntity); 337 338 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 339 if (outcome.getResource() != null) { 340 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(outcome.getResource()); 341 HookParams params = new HookParams() 342 .add(IPreResourceAccessDetails.class, accessDetails) 343 .add(RequestDetails.class, theRequest) 344 .addIfMatchesType(ServletRequestDetails.class, theRequest); 345 CompositeInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); 346 if (accessDetails.isDontReturnResourceAtIndex(0)) { 347 outcome.setResource(null); 348 } 349 } 350 351 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 352 // Note that this will only fire if someone actually goes to use the 353 // resource in a response (it's their responsibility to call 354 // outcome.fireResourceViewCallback()) 355 outcome.registerResourceViewCallback(() -> { 356 if (outcome.getResource() != null) { 357 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); 358 HookParams params = new HookParams() 359 .add(IPreResourceShowDetails.class, showDetails) 360 .add(RequestDetails.class, theRequest) 361 .addIfMatchesType(ServletRequestDetails.class, theRequest); 362 CompositeInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); 363 outcome.setResource(showDetails.getResource(0)); 364 } 365 }); 366 367 return outcome; 368 } 369 370 protected DaoMethodOutcome toMethodOutcomeLazy(RequestDetails theRequest, ResourcePersistentId theResourcePersistentId, @Nonnull final Supplier<LazyDaoMethodOutcome.EntityAndResource> theEntity, Supplier<IIdType> theIdSupplier) { 371 LazyDaoMethodOutcome outcome = new LazyDaoMethodOutcome(theResourcePersistentId); 372 373 outcome.setEntitySupplier(theEntity); 374 outcome.setIdSupplier(theIdSupplier); 375 outcome.setEntitySupplierUseCallback(() -> { 376 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 377 if (outcome.getResource() != null) { 378 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(outcome.getResource()); 379 HookParams params = new HookParams() 380 .add(IPreResourceAccessDetails.class, accessDetails) 381 .add(RequestDetails.class, theRequest) 382 .addIfMatchesType(ServletRequestDetails.class, theRequest); 383 CompositeInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); 384 if (accessDetails.isDontReturnResourceAtIndex(0)) { 385 outcome.setResource(null); 386 } 387 } 388 389 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 390 // Note that this will only fire if someone actually goes to use the 391 // resource in a response (it's their responsibility to call 392 // outcome.fireResourceViewCallback()) 393 outcome.registerResourceViewCallback(() -> { 394 if (outcome.getResource() != null) { 395 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); 396 HookParams params = new HookParams() 397 .add(IPreResourceShowDetails.class, showDetails) 398 .add(RequestDetails.class, theRequest) 399 .addIfMatchesType(ServletRequestDetails.class, theRequest); 400 CompositeInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); 401 outcome.setResource(showDetails.getResource(0)); 402 } 403 }); 404 }); 405 406 return outcome; 407 } 408 409 protected void doCallHooks(TransactionDetails theTransactionDetails, RequestDetails theRequestDetails, Pointcut thePointcut, HookParams theParams) { 410 if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts(thePointcut)) { 411 theTransactionDetails.addDeferredInterceptorBroadcast(thePointcut, theParams); 412 } else { 413 CompositeInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams); 414 } 415 } 416 417 protected abstract IInterceptorBroadcaster getInterceptorBroadcaster(); 418 419 public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) { 420 return createOperationOutcome(OO_SEVERITY_ERROR, theMessage, theCode); 421 } 422 423 public IBaseOperationOutcome createInfoOperationOutcome(String theMessage) { 424 return createOperationOutcome(OO_SEVERITY_INFO, theMessage, "informational"); 425 } 426 427 private IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode) { 428 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); 429 OperationOutcomeUtil.addIssue(getContext(), oo, theSeverity, theMessage, null, theCode); 430 return oo; 431 } 432 433 @Nonnull 434 protected ResourceGoneException createResourceGoneException(IBasePersistedResource theResourceEntity) { 435 StringBuilder b = new StringBuilder(); 436 b.append("Resource was deleted at "); 437 b.append(new InstantType(theResourceEntity.getDeleted()).getValueAsString()); 438 ResourceGoneException retVal = new ResourceGoneException(b.toString()); 439 retVal.setResourceId(theResourceEntity.getIdDt()); 440 return retVal; 441 } 442 443 /** 444 * Provide the DaoConfig 445 */ 446 protected abstract DaoConfig getConfig(); 447 448 /** 449 * Returns the resource type for this DAO, or null if this is a system-level DAO 450 */ 451 @Nullable 452 protected abstract String getResourceName(); 453 454 /** 455 * Provides the FHIR context 456 */ 457 protected abstract FhirContext getContext(); 458 459 @Transactional(propagation = Propagation.SUPPORTS) 460 public void translateRawParameters(Map<String, List<String>> theSource, SearchParameterMap theTarget) { 461 if (theSource == null || theSource.isEmpty()) { 462 return; 463 } 464 465 Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(getResourceName()); 466 467 Set<String> paramNames = theSource.keySet(); 468 for (String nextParamName : paramNames) { 469 QualifierDetails qualifiedParamName = QualifierDetails.extractQualifiersFromParameterName(nextParamName); 470 RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName()); 471 if (param == null) { 472 Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(getResourceName()); 473 String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), getResourceName(), validNames); 474 throw new InvalidRequestException(Msg.code(524) + msg); 475 } 476 477 // Should not be null since the check above would have caught it 478 RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(getResourceName(), qualifiedParamName.getParamName()); 479 480 for (String nextValue : theSource.get(nextParamName)) { 481 QualifiedParamList qualifiedParam = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifiedParamName.getWholeQualifier(), nextValue); 482 List<QualifiedParamList> paramList = Collections.singletonList(qualifiedParam); 483 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams(mySearchParamRegistry, getContext(), paramDef, nextParamName, paramList); 484 theTarget.add(qualifiedParamName.getParamName(), parsedParam); 485 } 486 487 } 488 } 489 490 public void notifyInterceptors(RestOperationTypeEnum theOperationType, IServerInterceptor.ActionRequestDetails theRequestDetails) { 491 if (theRequestDetails.getId() != null && theRequestDetails.getId().hasResourceType() && isNotBlank(theRequestDetails.getResourceType())) { 492 if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) { 493 throw new InternalErrorException(Msg.code(525) + "Inconsistent server state - Resource types don't match: " + theRequestDetails.getId().getResourceType() + " / " + theRequestDetails.getResourceType()); 494 } 495 } 496 497 if (theRequestDetails.getUserData().get(PROCESSING_SUB_REQUEST) == Boolean.TRUE) { 498 theRequestDetails.notifyIncomingRequestPreHandled(theOperationType); 499 } 500 } 501}