001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
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.jpa.searchparam.registry;
021
022import ca.uhn.fhir.context.ComboSearchParamType;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.model.api.ExtensionDt;
028import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
030import ca.uhn.fhir.util.DatatypeUtil;
031import ca.uhn.fhir.util.ExtensionUtil;
032import ca.uhn.fhir.util.FhirTerser;
033import ca.uhn.fhir.util.HapiExtensions;
034import ca.uhn.fhir.util.PhoneticEncoderUtil;
035import org.apache.commons.lang3.StringUtils;
036import org.hl7.fhir.dstu3.model.Extension;
037import org.hl7.fhir.dstu3.model.SearchParameter;
038import org.hl7.fhir.instance.model.api.IBase;
039import org.hl7.fhir.instance.model.api.IBaseDatatype;
040import org.hl7.fhir.instance.model.api.IBaseExtension;
041import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
042import org.hl7.fhir.instance.model.api.IBaseResource;
043import org.hl7.fhir.instance.model.api.IIdType;
044import org.hl7.fhir.instance.model.api.IPrimitiveType;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047import org.springframework.beans.factory.annotation.Autowired;
048import org.springframework.stereotype.Service;
049
050import java.util.ArrayList;
051import java.util.Collection;
052import java.util.Collections;
053import java.util.HashSet;
054import java.util.List;
055import java.util.Set;
056import java.util.stream.Collectors;
057
058import static org.apache.commons.lang3.StringUtils.isBlank;
059import static org.apache.commons.lang3.StringUtils.isNotBlank;
060import static org.apache.commons.lang3.StringUtils.startsWith;
061
062@Service
063public class SearchParameterCanonicalizer {
064        private static final Logger ourLog = LoggerFactory.getLogger(SearchParameterCanonicalizer.class);
065
066        private final FhirContext myFhirContext;
067        private final FhirTerser myTerser;
068
069        @Autowired
070        public SearchParameterCanonicalizer(FhirContext theFhirContext) {
071                myFhirContext = theFhirContext;
072                myTerser = myFhirContext.newTerser();
073        }
074
075        private static Collection<String> toStrings(Collection<? extends IPrimitiveType<String>> theBase) {
076                HashSet<String> retVal = new HashSet<>();
077                for (IPrimitiveType<String> next : theBase) {
078                        if (isNotBlank(next.getValueAsString())) {
079                                retVal.add(next.getValueAsString());
080                        }
081                }
082                return retVal;
083        }
084
085        public RuntimeSearchParam canonicalizeSearchParameter(IBaseResource theSearchParameter) {
086                RuntimeSearchParam retVal;
087                switch (myFhirContext.getVersion().getVersion()) {
088                        case DSTU2:
089                                retVal = canonicalizeSearchParameterDstu2(
090                                                (ca.uhn.fhir.model.dstu2.resource.SearchParameter) theSearchParameter);
091                                break;
092                        case DSTU3:
093                                retVal =
094                                                canonicalizeSearchParameterDstu3((org.hl7.fhir.dstu3.model.SearchParameter) theSearchParameter);
095                                break;
096                        case R4:
097                        case R4B:
098                        case R5:
099                                retVal = canonicalizeSearchParameterR4Plus(theSearchParameter);
100                                break;
101                        case DSTU2_HL7ORG:
102                        case DSTU2_1:
103                                // Non-supported - these won't happen so just fall through
104                        default:
105                                throw new InternalErrorException(
106                                                Msg.code(510) + "SearchParameter canonicalization not supported for FHIR version"
107                                                                + myFhirContext.getVersion().getVersion());
108                }
109
110                if (retVal != null) {
111                        extractExtensions(theSearchParameter, retVal);
112                }
113
114                return retVal;
115        }
116
117        private RuntimeSearchParam canonicalizeSearchParameterDstu2(
118                        ca.uhn.fhir.model.dstu2.resource.SearchParameter theNextSp) {
119                String name = theNextSp.getCode();
120                String description = theNextSp.getDescription();
121                String path = theNextSp.getXpath();
122
123                Collection<String> baseResource = toStrings(Collections.singletonList(theNextSp.getBaseElement()));
124                List<String> baseCustomResources = extractDstu2CustomResourcesFromExtensions(
125                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
126
127                if (!baseCustomResources.isEmpty()) {
128                        baseResource = Collections.singleton(baseCustomResources.get(0));
129                }
130
131                RestSearchParameterTypeEnum paramType = null;
132                RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
133                if (theNextSp.getTypeElement().getValueAsEnum() != null) {
134                        switch (theNextSp.getTypeElement().getValueAsEnum()) {
135                                case COMPOSITE:
136                                        paramType = RestSearchParameterTypeEnum.COMPOSITE;
137                                        break;
138                                case DATE_DATETIME:
139                                        paramType = RestSearchParameterTypeEnum.DATE;
140                                        break;
141                                case NUMBER:
142                                        paramType = RestSearchParameterTypeEnum.NUMBER;
143                                        break;
144                                case QUANTITY:
145                                        paramType = RestSearchParameterTypeEnum.QUANTITY;
146                                        break;
147                                case REFERENCE:
148                                        paramType = RestSearchParameterTypeEnum.REFERENCE;
149                                        break;
150                                case STRING:
151                                        paramType = RestSearchParameterTypeEnum.STRING;
152                                        break;
153                                case TOKEN:
154                                        paramType = RestSearchParameterTypeEnum.TOKEN;
155                                        break;
156                                case URI:
157                                        paramType = RestSearchParameterTypeEnum.URI;
158                                        break;
159                        }
160                }
161                if (theNextSp.getStatus() != null) {
162                        switch (theNextSp.getStatusElement().getValueAsEnum()) {
163                                case ACTIVE:
164                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
165                                        break;
166                                case DRAFT:
167                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT;
168                                        break;
169                                case RETIRED:
170                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED;
171                                        break;
172                        }
173                }
174
175                Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget());
176                List<String> targetCustomResources = extractDstu2CustomResourcesFromExtensions(
177                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
178
179                maybeAddCustomResourcesToResources(targetResources, targetCustomResources);
180
181                if (isBlank(name) || isBlank(path)) {
182                        if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
183                                return null;
184                        }
185                }
186
187                IIdType id = theNextSp.getIdElement();
188                String uri = "";
189                ComboSearchParamType unique = null;
190
191                List<ExtensionDt> uniqueExts = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE);
192                if (uniqueExts.size() > 0) {
193                        IPrimitiveType<?> uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive();
194                        if (uniqueExtsValuePrimitive != null) {
195                                if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
196                                        unique = ComboSearchParamType.UNIQUE;
197                                } else if ("false".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
198                                        unique = ComboSearchParamType.NON_UNIQUE;
199                                }
200                        }
201                }
202
203                List<RuntimeSearchParam.Component> components = Collections.emptyList();
204                return new RuntimeSearchParam(
205                                id,
206                                uri,
207                                name,
208                                description,
209                                path,
210                                paramType,
211                                Collections.emptySet(),
212                                targetResources,
213                                status,
214                                unique,
215                                components,
216                                baseResource);
217        }
218
219        private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) {
220                String name = theNextSp.getCode();
221                String description = theNextSp.getDescription();
222                String path = theNextSp.getExpression();
223
224                List<String> baseResources = new ArrayList<>(toStrings(theNextSp.getBase()));
225                List<String> baseCustomResources = extractDstu3CustomResourcesFromExtensions(
226                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
227
228                maybeAddCustomResourcesToResources(baseResources, baseCustomResources);
229
230                RestSearchParameterTypeEnum paramType = null;
231                RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
232                if (theNextSp.getType() != null) {
233                        switch (theNextSp.getType()) {
234                                case COMPOSITE:
235                                        paramType = RestSearchParameterTypeEnum.COMPOSITE;
236                                        break;
237                                case DATE:
238                                        paramType = RestSearchParameterTypeEnum.DATE;
239                                        break;
240                                case NUMBER:
241                                        paramType = RestSearchParameterTypeEnum.NUMBER;
242                                        break;
243                                case QUANTITY:
244                                        paramType = RestSearchParameterTypeEnum.QUANTITY;
245                                        break;
246                                case REFERENCE:
247                                        paramType = RestSearchParameterTypeEnum.REFERENCE;
248                                        break;
249                                case STRING:
250                                        paramType = RestSearchParameterTypeEnum.STRING;
251                                        break;
252                                case TOKEN:
253                                        paramType = RestSearchParameterTypeEnum.TOKEN;
254                                        break;
255                                case URI:
256                                        paramType = RestSearchParameterTypeEnum.URI;
257                                        break;
258                                case NULL:
259                                        break;
260                        }
261                }
262                if (theNextSp.getStatus() != null) {
263                        switch (theNextSp.getStatus()) {
264                                case ACTIVE:
265                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
266                                        break;
267                                case DRAFT:
268                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT;
269                                        break;
270                                case RETIRED:
271                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED;
272                                        break;
273                                case UNKNOWN:
274                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN;
275                                        break;
276                                case NULL:
277                                        break;
278                        }
279                }
280
281                Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget());
282                List<String> targetCustomResources = extractDstu3CustomResourcesFromExtensions(
283                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
284
285                maybeAddCustomResourcesToResources(targetResources, targetCustomResources);
286
287                if (isBlank(name) || isBlank(path) || paramType == null) {
288                        if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
289                                return null;
290                        }
291                }
292
293                IIdType id = theNextSp.getIdElement();
294                String uri = "";
295                ComboSearchParamType unique = null;
296
297                List<Extension> uniqueExts = theNextSp.getExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE);
298                if (uniqueExts.size() > 0) {
299                        IPrimitiveType<?> uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive();
300                        if (uniqueExtsValuePrimitive != null) {
301                                if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
302                                        unique = ComboSearchParamType.UNIQUE;
303                                } else if ("false".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
304                                        unique = ComboSearchParamType.NON_UNIQUE;
305                                }
306                        }
307                }
308
309                List<RuntimeSearchParam.Component> components = new ArrayList<>();
310                for (SearchParameter.SearchParameterComponentComponent next : theNextSp.getComponent()) {
311                        components.add(new RuntimeSearchParam.Component(
312                                        next.getExpression(),
313                                        next.getDefinition()
314                                                        .getReferenceElement()
315                                                        .toUnqualifiedVersionless()
316                                                        .getValue()));
317                }
318
319                return new RuntimeSearchParam(
320                                id,
321                                uri,
322                                name,
323                                description,
324                                path,
325                                paramType,
326                                Collections.emptySet(),
327                                targetResources,
328                                status,
329                                unique,
330                                components,
331                                baseResources);
332        }
333
334        private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNextSp) {
335
336                String name = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "code");
337                String description = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "description");
338                String path = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "expression");
339
340                Set<String> baseResources = extractR4PlusResources("base", theNextSp);
341                List<String> baseCustomResources = extractR4PlusCustomResourcesFromExtensions(
342                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
343
344                maybeAddCustomResourcesToResources(baseResources, baseCustomResources);
345
346                RestSearchParameterTypeEnum paramType = null;
347                RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
348                switch (myTerser.getSinglePrimitiveValue(theNextSp, "type").orElse("")) {
349                        case "composite":
350                                paramType = RestSearchParameterTypeEnum.COMPOSITE;
351                                break;
352                        case "date":
353                                paramType = RestSearchParameterTypeEnum.DATE;
354                                break;
355                        case "number":
356                                paramType = RestSearchParameterTypeEnum.NUMBER;
357                                break;
358                        case "quantity":
359                                paramType = RestSearchParameterTypeEnum.QUANTITY;
360                                break;
361                        case "reference":
362                                paramType = RestSearchParameterTypeEnum.REFERENCE;
363                                break;
364                        case "string":
365                                paramType = RestSearchParameterTypeEnum.STRING;
366                                break;
367                        case "token":
368                                paramType = RestSearchParameterTypeEnum.TOKEN;
369                                break;
370                        case "uri":
371                                paramType = RestSearchParameterTypeEnum.URI;
372                                break;
373                        case "special":
374                                paramType = RestSearchParameterTypeEnum.SPECIAL;
375                                break;
376                }
377                switch (myTerser.getSinglePrimitiveValue(theNextSp, "status").orElse("")) {
378                        case "active":
379                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
380                                break;
381                        case "draft":
382                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT;
383                                break;
384                        case "retired":
385                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED;
386                                break;
387                        case "unknown":
388                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN;
389                                break;
390                }
391
392                Set<String> targetResources = extractR4PlusResources("target", theNextSp);
393                List<String> targetCustomResources = extractR4PlusCustomResourcesFromExtensions(
394                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
395
396                maybeAddCustomResourcesToResources(targetResources, targetCustomResources);
397
398                if (isBlank(name) || isBlank(path) || paramType == null) {
399                        if ("_text".equals(name) || "_content".equals(name)) {
400                                // ok
401                        } else if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
402                                return null;
403                        }
404                }
405
406                IIdType id = theNextSp.getIdElement();
407                String uri = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "url");
408                ComboSearchParamType unique = null;
409
410                String value = ((IBaseHasExtensions) theNextSp)
411                                .getExtension().stream()
412                                                .filter(e -> HapiExtensions.EXT_SP_UNIQUE.equals(e.getUrl()))
413                                                .filter(t -> t.getValue() instanceof IPrimitiveType)
414                                                .map(t -> (IPrimitiveType<?>) t.getValue())
415                                                .map(t -> t.getValueAsString())
416                                                .findFirst()
417                                                .orElse("");
418                if ("true".equalsIgnoreCase(value)) {
419                        unique = ComboSearchParamType.UNIQUE;
420                } else if ("false".equalsIgnoreCase(value)) {
421                        unique = ComboSearchParamType.NON_UNIQUE;
422                }
423
424                List<RuntimeSearchParam.Component> components = new ArrayList<>();
425                for (IBase next : myTerser.getValues(theNextSp, "component")) {
426                        String expression = myTerser.getSinglePrimitiveValueOrNull(next, "expression");
427                        String definition = myTerser.getSinglePrimitiveValueOrNull(next, "definition");
428                        if (startsWith(definition, "/SearchParameter/")) {
429                                definition = definition.substring(1);
430                        }
431
432                        components.add(new RuntimeSearchParam.Component(expression, definition));
433                }
434
435                return new RuntimeSearchParam(
436                                id,
437                                uri,
438                                name,
439                                description,
440                                path,
441                                paramType,
442                                Collections.emptySet(),
443                                targetResources,
444                                status,
445                                unique,
446                                components,
447                                baseResources);
448        }
449
450        private Set<String> extractR4PlusResources(String thePath, IBaseResource theNextSp) {
451                return myTerser.getValues(theNextSp, thePath, IPrimitiveType.class).stream()
452                                .map(IPrimitiveType::getValueAsString)
453                                .collect(Collectors.toSet());
454        }
455
456        /**
457         * Extracts any extensions from the resource and populates an extension field in the
458         */
459        protected void extractExtensions(IBaseResource theSearchParamResource, RuntimeSearchParam theRuntimeSearchParam) {
460                if (theSearchParamResource instanceof IBaseHasExtensions) {
461                        List<? extends IBaseExtension<? extends IBaseExtension, ?>> extensions =
462                                        (List<? extends IBaseExtension<? extends IBaseExtension, ?>>)
463                                                        ((IBaseHasExtensions) theSearchParamResource).getExtension();
464                        for (IBaseExtension<? extends IBaseExtension, ?> next : extensions) {
465                                String nextUrl = next.getUrl();
466                                if (isNotBlank(nextUrl)) {
467                                        theRuntimeSearchParam.addExtension(nextUrl, next);
468                                        if (HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER.equals(nextUrl)) {
469                                                setEncoder(theRuntimeSearchParam, next.getValue());
470                                        } else if (HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN.equals(nextUrl)) {
471                                                addUpliftRefchain(theRuntimeSearchParam, next);
472                                        }
473                                }
474                        }
475                }
476        }
477
478        @SuppressWarnings("unchecked")
479        private void addUpliftRefchain(
480                        RuntimeSearchParam theRuntimeSearchParam, IBaseExtension<? extends IBaseExtension, ?> theExtension) {
481                String code = ExtensionUtil.extractChildPrimitiveExtensionValue(
482                                theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE);
483                String elementName = ExtensionUtil.extractChildPrimitiveExtensionValue(
484                                theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME);
485                if (isNotBlank(code)) {
486                        theRuntimeSearchParam.addUpliftRefchain(code, elementName);
487                }
488        }
489
490        private void setEncoder(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype theValue) {
491                if (theValue instanceof IPrimitiveType) {
492                        String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString();
493
494                        // every string creates a completely new encoder wrapper.
495                        // this is fine, because the runtime search parameters are constructed at startup
496                        // for every saved value
497                        IPhoneticEncoder encoder = PhoneticEncoderUtil.getEncoder(stringValue);
498                        if (encoder != null) {
499                                theRuntimeSearchParam.setPhoneticEncoder(encoder);
500                        } else {
501                                ourLog.error("Invalid PhoneticEncoderEnum value '" + stringValue + "'");
502                        }
503                }
504        }
505
506        private List<String> extractDstu2CustomResourcesFromExtensions(
507                        ca.uhn.fhir.model.dstu2.resource.SearchParameter theSearchParameter, String theExtensionUrl) {
508
509                List<ExtensionDt> customSpExtensionDt = theSearchParameter.getUndeclaredExtensionsByUrl(theExtensionUrl);
510
511                return customSpExtensionDt.stream()
512                                .map(theExtensionDt -> theExtensionDt.getValueAsPrimitive().getValueAsString())
513                                .filter(StringUtils::isNotBlank)
514                                .collect(Collectors.toList());
515        }
516
517        private List<String> extractDstu3CustomResourcesFromExtensions(
518                        org.hl7.fhir.dstu3.model.SearchParameter theSearchParameter, String theExtensionUrl) {
519
520                List<Extension> customSpExtensions = theSearchParameter.getExtensionsByUrl(theExtensionUrl);
521
522                return customSpExtensions.stream()
523                                .map(theExtension -> theExtension.getValueAsPrimitive().getValueAsString())
524                                .filter(StringUtils::isNotBlank)
525                                .collect(Collectors.toList());
526        }
527
528        private List<String> extractR4PlusCustomResourcesFromExtensions(
529                        IBaseResource theSearchParameter, String theExtensionUrl) {
530
531                List<String> retVal = new ArrayList<>();
532
533                if (theSearchParameter instanceof IBaseHasExtensions) {
534                        ((IBaseHasExtensions) theSearchParameter)
535                                        .getExtension().stream()
536                                                        .filter(t -> theExtensionUrl.equals(t.getUrl()))
537                                                        .filter(t -> t.getValue() instanceof IPrimitiveType)
538                                                        .map(t -> ((IPrimitiveType<?>) t.getValue()))
539                                                        .map(IPrimitiveType::getValueAsString)
540                                                        .filter(StringUtils::isNotBlank)
541                                                        .forEach(retVal::add);
542                }
543
544                return retVal;
545        }
546
547        private <T extends Collection<String>> void maybeAddCustomResourcesToResources(
548                        T theResources, List<String> theCustomResources) {
549                // SearchParameter base and target components require strict binding to ResourceType for dstu[2|3], R4, R4B
550                // and to Version Independent Resource Types for R5.
551                //
552                // To handle custom resources, we set a placeholder of type 'Resource' in the base or target component and
553                // define
554                // the custom resource by adding a corresponding extension with url
555                // HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE
556                // or HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE with the name of the custom resource.
557                //
558                // To provide a base/target list that contains both the resources and customResources, we need to remove the
559                // placeholders
560                // from the theResources and add theCustomResources.
561
562                if (!theCustomResources.isEmpty()) {
563                        theResources.removeAll(Collections.singleton("Resource"));
564                        theResources.addAll(theCustomResources);
565                }
566        }
567}