001/*
002 * #%L
003 * HAPI FHIR Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2023 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.extractor;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.jpa.model.config.PartitionSettings;
024import ca.uhn.fhir.jpa.model.entity.StorageSettings;
025import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
026import ca.uhn.fhir.sl.cache.Cache;
027import ca.uhn.fhir.sl.cache.CacheFactory;
028import com.google.common.annotations.VisibleForTesting;
029import org.hl7.fhir.exceptions.FHIRException;
030import org.hl7.fhir.exceptions.PathEngineException;
031import org.hl7.fhir.instance.model.api.IBase;
032import org.hl7.fhir.r4.context.IWorkerContext;
033import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
034import org.hl7.fhir.r4.utils.FHIRPathEngine;
035import org.hl7.fhir.r4.model.Base;
036import org.hl7.fhir.r4.model.ExpressionNode;
037import org.hl7.fhir.r4.model.IdType;
038import org.hl7.fhir.r4.model.Resource;
039import org.hl7.fhir.r4.model.ResourceType;
040import org.hl7.fhir.r4.model.TypeDetails;
041import org.hl7.fhir.r4.model.ValueSet;
042import org.hl7.fhir.r4.utils.FHIRPathEngine;
043
044import javax.annotation.PostConstruct;
045import java.util.Collections;
046import java.util.HashMap;
047import java.util.List;
048import java.util.Map;
049import java.util.concurrent.TimeUnit;
050
051import static org.apache.commons.lang3.StringUtils.isNotBlank;
052
053public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor {
054
055        private Cache<String, ExpressionNode> myParsedFhirPathCache;
056        private FHIRPathEngine myFhirPathEngine;
057
058        /**
059         * Constructor
060         */
061        public SearchParamExtractorR4() {
062                super();
063        }
064
065        // This constructor is used by tests
066        @VisibleForTesting
067        public SearchParamExtractorR4(StorageSettings theStorageSettings, PartitionSettings thePartitionSettings, FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) {
068                super(theStorageSettings, thePartitionSettings, theCtx, theSearchParamRegistry);
069                initFhirPath();
070                start();
071        }
072
073        @Override
074        public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
075                return () -> {
076                        ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
077                        return myFhirPathEngine.evaluate(theResource, (Base) theResource, (Base) theResource, (Base) theResource, parsed);
078                };
079        }
080
081
082        @Override
083        @PostConstruct
084        public void start() {
085                super.start();
086                if (myFhirPathEngine == null) {
087                        initFhirPath();
088                }
089        }
090
091        public void initFhirPath() {
092                IWorkerContext worker = new HapiWorkerContext(getContext(), getContext().getValidationSupport());
093                myFhirPathEngine = new FHIRPathEngine(worker);
094                myFhirPathEngine.setHostServices(new SearchParamExtractorR4HostServices());
095
096                myParsedFhirPathCache = CacheFactory.build(TimeUnit.MINUTES.toMillis(10));
097        }
098
099
100        private class SearchParamExtractorR4HostServices implements FHIRPathEngine.IEvaluationContext {
101
102                private final Map<String, Base> myResourceTypeToStub = Collections.synchronizedMap(new HashMap<>());
103
104                @Override
105                public List<Base> resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
106                        return Collections.emptyList();
107                }
108
109                @Override
110                public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
111                        return null;
112                }
113
114                @Override
115                public boolean log(String argument, List<Base> focus) {
116                        return false;
117                }
118
119                @Override
120                public FunctionDetails resolveFunction(String functionName) {
121                        return null;
122                }
123
124                @Override
125                public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
126                        return null;
127                }
128
129                @Override
130                public List<Base> executeFunction(Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) {
131                        return null;
132                }
133
134
135                @Override
136                public Base resolveReference(Object theAppContext, String theUrl, Base theRefContext) throws FHIRException {
137                        Base retVal = resolveResourceInBundleWithPlaceholderId(theAppContext, theUrl);
138                        if (retVal != null) {
139                                return retVal;
140                        }
141
142                        /*
143                         * When we're doing resolution within the SearchParamExtractor, if we want
144                         * to do a resolve() it's just to check the type, so there is no point
145                         * going through the heavyweight test. We can just return a stub and
146                         * that's good enough since we're just doing something like
147                         *    Encounter.patient.where(resolve() is Patient)
148                         */
149                        IdType url = new IdType(theUrl);
150                        if (isNotBlank(url.getResourceType())) {
151
152                                retVal = myResourceTypeToStub.get(url.getResourceType());
153                                if (retVal != null) {
154                                        return retVal;
155                                }
156
157                                ResourceType resourceType = ResourceType.fromCode(url.getResourceType());
158                                if (resourceType != null) {
159                                        retVal = new Resource() {
160                                                private static final long serialVersionUID = -5303169871827706447L;
161
162                                                @Override
163                                                public Resource copy() {
164                                                        return this;
165                                                }
166
167                                                @Override
168                                                public ResourceType getResourceType() {
169                                                        return resourceType;
170                                                }
171
172                                                @Override
173                                                public String fhirType() {
174                                                        return url.getResourceType();
175                                                }
176
177                                        };
178                                        myResourceTypeToStub.put(url.getResourceType(), retVal);
179                                }
180                        }
181                        return retVal;
182                }
183
184                @Override
185                public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
186                        return false;
187                }
188
189                @Override
190                public ValueSet resolveValueSet(Object appContext, String url) {
191                        return null;
192                }
193        }
194
195}