001/*
002 * #%L
003 * HAPI FHIR JPA Server
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.provider.dstu3;
021
022import ca.uhn.fhir.context.RuntimeSearchParam;
023import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
024import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
025import ca.uhn.fhir.rest.api.Constants;
026import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
027import ca.uhn.fhir.rest.api.server.RequestDetails;
028import ca.uhn.fhir.rest.server.RestfulServer;
029import ca.uhn.fhir.rest.server.RestfulServerConfiguration;
030import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
031import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
032import ca.uhn.fhir.util.CoverageIgnore;
033import ca.uhn.fhir.util.ExtensionConstants;
034import jakarta.servlet.http.HttpServletRequest;
035import org.hl7.fhir.dstu3.model.Bundle;
036import org.hl7.fhir.dstu3.model.CapabilityStatement;
037import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestComponent;
038import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceComponent;
039import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent;
040import org.hl7.fhir.dstu3.model.CapabilityStatement.ConditionalDeleteStatus;
041import org.hl7.fhir.dstu3.model.CapabilityStatement.ResourceVersionPolicy;
042import org.hl7.fhir.dstu3.model.DecimalType;
043import org.hl7.fhir.dstu3.model.Enumerations.SearchParamType;
044import org.hl7.fhir.dstu3.model.Extension;
045import org.hl7.fhir.dstu3.model.Meta;
046import org.hl7.fhir.dstu3.model.UriType;
047
048import java.util.Collections;
049import java.util.List;
050import java.util.Map;
051import java.util.Set;
052import java.util.stream.Collectors;
053
054import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
055import static org.apache.commons.lang3.StringUtils.isBlank;
056import static org.apache.commons.lang3.StringUtils.isNotBlank;
057
058public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.server.ServerCapabilityStatementProvider {
059
060        private volatile CapabilityStatement myCachedValue;
061        private JpaStorageSettings myStorageSettings;
062        private ISearchParamRegistry mySearchParamRegistry;
063        private String myImplementationDescription;
064        private boolean myIncludeResourceCounts;
065        private RestfulServer myRestfulServer;
066        private IFhirSystemDao<Bundle, Meta> mySystemDao;
067
068        private RestfulServerConfiguration myServerConfiguration;
069
070        /**
071         * Constructor
072         */
073        @CoverageIgnore
074        public JpaConformanceProviderDstu3() {
075                super();
076                super.setCache(false);
077                setIncludeResourceCounts(true);
078        }
079
080        /**
081         * Constructor
082         */
083        public JpaConformanceProviderDstu3(
084                        RestfulServer theRestfulServer,
085                        IFhirSystemDao<Bundle, Meta> theSystemDao,
086                        JpaStorageSettings theStorageSettings,
087                        ISearchParamRegistry theSearchParamRegistry) {
088                super(theRestfulServer);
089                myRestfulServer = theRestfulServer;
090                mySystemDao = theSystemDao;
091                myStorageSettings = theStorageSettings;
092                myServerConfiguration = theRestfulServer.createConfiguration();
093                super.setCache(false);
094                setSearchParamRegistry(theSearchParamRegistry);
095                setIncludeResourceCounts(true);
096        }
097
098        public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
099                mySearchParamRegistry = theSearchParamRegistry;
100        }
101
102        @Override
103        public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
104                CapabilityStatement retVal = myCachedValue;
105
106                Map<String, Long> counts = null;
107                if (myIncludeResourceCounts) {
108                        counts = mySystemDao.getResourceCountsFromCache();
109                }
110                counts = defaultIfNull(counts, Collections.emptyMap());
111
112                retVal = super.getServerConformance(theRequest, theRequestDetails);
113                for (CapabilityStatementRestComponent nextRest : retVal.getRest()) {
114
115                        for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) {
116
117                                nextResource.setVersioning(ResourceVersionPolicy.VERSIONEDUPDATE);
118
119                                ConditionalDeleteStatus conditionalDelete = nextResource.getConditionalDelete();
120                                if (conditionalDelete == ConditionalDeleteStatus.MULTIPLE
121                                                && myStorageSettings.isAllowMultipleDelete() == false) {
122                                        nextResource.setConditionalDelete(ConditionalDeleteStatus.SINGLE);
123                                }
124
125                                // Add resource counts
126                                Long count = counts.get(nextResource.getTypeElement().getValueAsString());
127                                if (count != null) {
128                                        nextResource.addExtension(
129                                                        new Extension(ExtensionConstants.CONF_RESOURCE_COUNT, new DecimalType(count)));
130                                }
131
132                                nextResource.getSearchParam().clear();
133                                String resourceName = nextResource.getType();
134                                ResourceSearchParams searchParams = constructCompleteSearchParamList(resourceName);
135                                for (RuntimeSearchParam runtimeSp : searchParams.values()) {
136                                        CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam();
137
138                                        confSp.setName(runtimeSp.getName());
139                                        confSp.setDocumentation(runtimeSp.getDescription());
140                                        confSp.setDefinition(runtimeSp.getUri());
141                                        switch (runtimeSp.getParamType()) {
142                                                case COMPOSITE:
143                                                        confSp.setType(SearchParamType.COMPOSITE);
144                                                        break;
145                                                case DATE:
146                                                        confSp.setType(SearchParamType.DATE);
147                                                        break;
148                                                case NUMBER:
149                                                        confSp.setType(SearchParamType.NUMBER);
150                                                        break;
151                                                case QUANTITY:
152                                                        confSp.setType(SearchParamType.QUANTITY);
153                                                        break;
154                                                case REFERENCE:
155                                                        confSp.setType(SearchParamType.REFERENCE);
156                                                        break;
157                                                case STRING:
158                                                        confSp.setType(SearchParamType.STRING);
159                                                        break;
160                                                case TOKEN:
161                                                        confSp.setType(SearchParamType.TOKEN);
162                                                        break;
163                                                case URI:
164                                                        confSp.setType(SearchParamType.URI);
165                                                        break;
166                                                case HAS:
167                                                        // Shouldn't happen
168                                                        break;
169                                        }
170                                }
171
172                                updateIncludesList(nextResource, searchParams);
173                                updateRevIncludesList(nextResource, searchParams);
174                        }
175                }
176
177                massage(retVal);
178
179                if (myStorageSettings
180                                .getSupportedSubscriptionTypes()
181                                .contains(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.WEBSOCKET)) {
182                        if (isNotBlank(myStorageSettings.getWebsocketContextPath())) {
183                                Extension websocketExtension = new Extension();
184                                websocketExtension.setUrl(Constants.CAPABILITYSTATEMENT_WEBSOCKET_URL);
185                                websocketExtension.setValue(new UriType(myStorageSettings.getWebsocketContextPath()));
186                                retVal.getRestFirstRep().addExtension(websocketExtension);
187                        }
188                }
189
190                retVal.getImplementation().setDescription(myImplementationDescription);
191                myCachedValue = retVal;
192                return retVal;
193        }
194
195        private ResourceSearchParams constructCompleteSearchParamList(String theResourceName) {
196                // Borrowed from
197                // hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java
198
199                /*
200                 * If we have an explicit registry (which will be the case in the JPA server) we use it as priority,
201                 * but also fill in any gaps using params from the server itself. This makes sure we include
202                 * global params like _lastUpdated
203                 */
204                ResourceSearchParams searchParams;
205                ResourceSearchParams serverConfigurationActiveSearchParams =
206                                myServerConfiguration.getActiveSearchParams(theResourceName);
207                if (mySearchParamRegistry != null) {
208                        searchParams =
209                                        mySearchParamRegistry.getActiveSearchParams(theResourceName).makeCopy();
210                        if (searchParams == null) {
211                                return ResourceSearchParams.empty(theResourceName);
212                        }
213                        for (String nextBuiltInSpName : serverConfigurationActiveSearchParams.getSearchParamNames()) {
214                                if (nextBuiltInSpName.startsWith("_")
215                                                && !searchParams.containsParamName(nextBuiltInSpName)
216                                                && searchParamEnabled(nextBuiltInSpName)) {
217                                        searchParams.put(nextBuiltInSpName, serverConfigurationActiveSearchParams.get(nextBuiltInSpName));
218                                }
219                        }
220                } else {
221                        searchParams = serverConfigurationActiveSearchParams;
222                }
223
224                return searchParams;
225        }
226
227        protected boolean searchParamEnabled(String theSearchParam) {
228                // Borrowed from
229                // hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java
230                return !Constants.PARAM_FILTER.equals(theSearchParam) || myStorageSettings.isFilterParameterEnabled();
231        }
232
233        private void updateRevIncludesList(
234                        CapabilityStatementRestResourceComponent theNextResource, ResourceSearchParams theSearchParams) {
235                // Add RevInclude to CapabilityStatement.rest.resource
236                if (theNextResource.getSearchRevInclude().isEmpty()) {
237                        String resourcename = theNextResource.getType();
238                        Set<String> allResourceTypes =
239                                        myServerConfiguration.collectMethodBindings().keySet();
240                        for (String otherResourceType : allResourceTypes) {
241                                if (isBlank(otherResourceType)) {
242                                        continue;
243                                }
244                                ResourceSearchParams activeSearchParams =
245                                                mySearchParamRegistry.getActiveSearchParams(otherResourceType);
246                                activeSearchParams.values().stream()
247                                                .filter(t -> isNotBlank(t.getName()))
248                                                .filter(t -> t.getTargets().contains(resourcename))
249                                                .forEach(t -> theNextResource.addSearchRevInclude(otherResourceType + ":" + t.getName()));
250                        }
251                }
252        }
253
254        private void updateIncludesList(
255                        CapabilityStatementRestResourceComponent theResource, ResourceSearchParams theSearchParams) {
256                // Borrowed from
257                // hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java
258                String resourceName = theResource.getType();
259                if (theResource.getSearchInclude().isEmpty()) {
260                        List<String> includes = theSearchParams.values().stream()
261                                        .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
262                                        .map(t -> resourceName + ":" + t.getName())
263                                        .sorted()
264                                        .collect(Collectors.toList());
265                        theResource.addSearchInclude("*");
266                        for (String nextInclude : includes) {
267                                theResource.addSearchInclude(nextInclude);
268                        }
269                }
270        }
271
272        public boolean isIncludeResourceCounts() {
273                return myIncludeResourceCounts;
274        }
275
276        public void setIncludeResourceCounts(boolean theIncludeResourceCounts) {
277                myIncludeResourceCounts = theIncludeResourceCounts;
278        }
279
280        /**
281         * Subclasses may override
282         */
283        protected void massage(CapabilityStatement theStatement) {
284                // nothing
285        }
286
287        public void setStorageSettings(JpaStorageSettings theStorageSettings) {
288                this.myStorageSettings = theStorageSettings;
289        }
290
291        @CoverageIgnore
292        public void setImplementationDescription(String theImplDesc) {
293                myImplementationDescription = theImplDesc;
294        }
295
296        @Override
297        public void setRestfulServer(RestfulServer theRestfulServer) {
298                this.myRestfulServer = theRestfulServer;
299                super.setRestfulServer(theRestfulServer);
300        }
301
302        @CoverageIgnore
303        public void setSystemDao(IFhirSystemDao<Bundle, Meta> mySystemDao) {
304                this.mySystemDao = mySystemDao;
305        }
306}