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}