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}