001/*
002 * #%L
003 * HAPI FHIR - Server Framework
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.rest.server.method;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.model.api.IResource;
025import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
026import ca.uhn.fhir.model.primitive.IdDt;
027import ca.uhn.fhir.model.valueset.BundleTypeEnum;
028import ca.uhn.fhir.rest.annotation.History;
029import ca.uhn.fhir.rest.api.Constants;
030import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
031import ca.uhn.fhir.rest.api.server.IBundleProvider;
032import ca.uhn.fhir.rest.api.server.IRestfulServer;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.param.ParameterUtil;
035import ca.uhn.fhir.rest.server.IResourceProvider;
036import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
038import jakarta.annotation.Nonnull;
039import org.apache.commons.lang3.StringUtils;
040import org.hl7.fhir.instance.model.api.IBaseResource;
041import org.hl7.fhir.instance.model.api.IPrimitiveType;
042
043import java.lang.reflect.Method;
044import java.lang.reflect.Modifier;
045import java.util.Date;
046import java.util.List;
047
048import static org.apache.commons.lang3.StringUtils.isBlank;
049
050public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
051
052        private final Integer myIdParamIndex;
053        private final RestOperationTypeEnum myResourceOperationType;
054        private final String myResourceName;
055
056        public HistoryMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
057                super(toReturnType(theMethod, theProvider), theMethod, theContext, theProvider);
058
059                myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
060
061                History historyAnnotation = theMethod.getAnnotation(History.class);
062                Class<? extends IBaseResource> type = historyAnnotation.type();
063                if (Modifier.isInterface(type.getModifiers())) {
064                        if (theProvider instanceof IResourceProvider) {
065                                type = ((IResourceProvider) theProvider).getResourceType();
066                                if (myIdParamIndex != null) {
067                                        myResourceOperationType = RestOperationTypeEnum.HISTORY_INSTANCE;
068                                } else {
069                                        myResourceOperationType = RestOperationTypeEnum.HISTORY_TYPE;
070                                }
071                        } else {
072                                myResourceOperationType = RestOperationTypeEnum.HISTORY_SYSTEM;
073                        }
074                } else {
075                        if (myIdParamIndex != null) {
076                                myResourceOperationType = RestOperationTypeEnum.HISTORY_INSTANCE;
077                        } else {
078                                myResourceOperationType = RestOperationTypeEnum.HISTORY_TYPE;
079                        }
080                }
081
082                if (type != IBaseResource.class && type != IResource.class) {
083                        myResourceName = theContext.getResourceType(type);
084                } else {
085                        myResourceName = null;
086                }
087        }
088
089        @Override
090        protected BundleTypeEnum getResponseBundleType() {
091                return BundleTypeEnum.HISTORY;
092        }
093
094        @Nonnull
095        @Override
096        public RestOperationTypeEnum getRestOperationType() {
097                return myResourceOperationType;
098        }
099
100        @Override
101        public ReturnTypeEnum getReturnType() {
102                return ReturnTypeEnum.BUNDLE;
103        }
104
105        @Override
106        protected boolean isOffsetModeHistory() {
107                return true;
108        }
109
110        // ObjectUtils.equals is replaced by a JDK7 method..
111        @Override
112        public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
113                if (!Constants.PARAM_HISTORY.equals(theRequest.getOperation())) {
114                        return MethodMatchEnum.NONE;
115                }
116                if (theRequest.getResourceName() == null) {
117                        if (myResourceOperationType == RestOperationTypeEnum.HISTORY_SYSTEM) {
118                                return MethodMatchEnum.EXACT;
119                        } else {
120                                return MethodMatchEnum.NONE;
121                        }
122                }
123                if (!StringUtils.equals(theRequest.getResourceName(), myResourceName)) {
124                        return MethodMatchEnum.NONE;
125                }
126
127                boolean haveIdParam = theRequest.getId() != null && !theRequest.getId().isEmpty();
128                boolean wantIdParam = myIdParamIndex != null;
129                if (haveIdParam != wantIdParam) {
130                        return MethodMatchEnum.NONE;
131                }
132
133                if (theRequest.getId() == null) {
134                        if (myResourceOperationType != RestOperationTypeEnum.HISTORY_TYPE) {
135                                return MethodMatchEnum.NONE;
136                        }
137                } else if (theRequest.getId().hasVersionIdPart()) {
138                        return MethodMatchEnum.NONE;
139                }
140
141                return MethodMatchEnum.EXACT;
142        }
143
144        @Override
145        public IBundleProvider invokeServer(
146                        IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams)
147                        throws InvalidRequestException, InternalErrorException {
148                if (myIdParamIndex != null) {
149                        theMethodParams[myIdParamIndex] = theRequest.getId();
150                }
151
152                Object response = invokeServerMethod(theRequest, theMethodParams);
153
154                final IBundleProvider resources = toResourceList(response);
155
156                /*
157                 * We wrap the response so we can verify that it has the ID and version set,
158                 * as is the contract for history
159                 */
160                return new IBundleProvider() {
161
162                        @Override
163                        public String getCurrentPageId() {
164                                return resources.getCurrentPageId();
165                        }
166
167                        @Override
168                        public String getNextPageId() {
169                                return resources.getNextPageId();
170                        }
171
172                        @Override
173                        public String getPreviousPageId() {
174                                return resources.getPreviousPageId();
175                        }
176
177                        @Override
178                        public IPrimitiveType<Date> getPublished() {
179                                return resources.getPublished();
180                        }
181
182                        @Nonnull
183                        @Override
184                        public List<IBaseResource> getResources(
185                                        int theFromIndex, int theToIndex, ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
186                                List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex, theResponsePageBuilder);
187                                int index = theFromIndex;
188                                for (IBaseResource nextResource : retVal) {
189                                        if (nextResource.getIdElement() == null
190                                                        || isBlank(nextResource.getIdElement().getIdPart())) {
191                                                throw new InternalErrorException(Msg.code(410) + "Server provided resource at index " + index
192                                                                + " with no ID set (using IResource#setId(IdDt))");
193                                        }
194                                        if (isBlank(nextResource.getIdElement().getVersionIdPart()) && nextResource instanceof IResource) {
195                                                // TODO: Use of a deprecated method should be resolved.
196                                                IdDt versionId = ResourceMetadataKeyEnum.VERSION_ID.get(nextResource);
197                                                if (versionId == null || versionId.isEmpty()) {
198                                                        throw new InternalErrorException(Msg.code(411) + "Server provided resource at index "
199                                                                        + index + " with no Version ID set (using IResource#setId(IdDt))");
200                                                }
201                                        }
202                                        index++;
203                                }
204                                return retVal;
205                        }
206
207                        @Override
208                        public String getUuid() {
209                                return resources.getUuid();
210                        }
211
212                        @Override
213                        public Integer preferredPageSize() {
214                                return resources.preferredPageSize();
215                        }
216
217                        @Override
218                        public Integer size() {
219                                return resources.size();
220                        }
221                };
222        }
223
224        private static Class<? extends IBaseResource> toReturnType(Method theMethod, Object theProvider) {
225                if (theProvider instanceof IResourceProvider) {
226                        return ((IResourceProvider) theProvider).getResourceType();
227                }
228                History historyAnnotation = theMethod.getAnnotation(History.class);
229                Class<? extends IBaseResource> type = historyAnnotation.type();
230                if (type != IBaseResource.class && type != IResource.class) {
231                        return type;
232                }
233                return null;
234        }
235}