001package ca.uhn.fhir.rest.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022import static org.apache.commons.lang3.StringUtils.isBlank;
023import static org.apache.commons.lang3.StringUtils.isNotBlank;
024
025import java.lang.reflect.Method;
026import java.lang.reflect.Modifier;
027import java.util.Date;
028import java.util.List;
029
030import org.apache.commons.lang3.StringUtils;
031import org.hl7.fhir.instance.model.api.IBaseResource;
032import org.hl7.fhir.instance.model.api.IPrimitiveType;
033
034import ca.uhn.fhir.context.FhirContext;
035import ca.uhn.fhir.model.api.IResource;
036import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
037import ca.uhn.fhir.model.primitive.IdDt;
038import ca.uhn.fhir.model.primitive.InstantDt;
039import ca.uhn.fhir.model.valueset.BundleTypeEnum;
040import ca.uhn.fhir.rest.annotation.History;
041import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
042import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
043import ca.uhn.fhir.rest.server.Constants;
044import ca.uhn.fhir.rest.server.IBundleProvider;
045import ca.uhn.fhir.rest.server.IResourceProvider;
046import ca.uhn.fhir.rest.server.IRestfulServer;
047import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
048import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
049
050public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
051
052        private final Integer myIdParamIndex;
053        private String myResourceName;
054        private final RestOperationTypeEnum myResourceOperationType;
055
056        public HistoryMethodBinding(Method theMethod, FhirContext theConetxt, Object theProvider) {
057                super(toReturnType(theMethod, theProvider), theMethod, theConetxt, theProvider);
058
059                myIdParamIndex = MethodUtil.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 != IResource.class) {
083                        myResourceName = theConetxt.getResourceDefinition(type).getName();
084                } else {
085                        myResourceName = null;
086                }
087
088        }
089
090        @Override
091        public RestOperationTypeEnum getRestOperationType() {
092                return myResourceOperationType;
093        }
094
095        @Override
096        protected BundleTypeEnum getResponseBundleType() {
097                return BundleTypeEnum.HISTORY;
098        }
099
100        @Override
101        public ReturnTypeEnum getReturnType() {
102                return ReturnTypeEnum.BUNDLE;
103        }
104
105        // ObjectUtils.equals is replaced by a JDK7 method..
106        @Override
107        public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
108                if (!Constants.PARAM_HISTORY.equals(theRequest.getOperation())) {
109                        return false;
110                }
111                if (theRequest.getResourceName() == null) {
112                        return myResourceOperationType == RestOperationTypeEnum.HISTORY_SYSTEM;
113                }
114                if (!StringUtils.equals(theRequest.getResourceName(), myResourceName)) {
115                        return false;
116                }
117
118                boolean haveIdParam = theRequest.getId() != null && !theRequest.getId().isEmpty();
119                boolean wantIdParam = myIdParamIndex != null;
120                if (haveIdParam != wantIdParam) {
121                        return false;
122                }
123
124                if (theRequest.getId() == null) {
125                        return myResourceOperationType == RestOperationTypeEnum.HISTORY_TYPE;
126                } else if (theRequest.getId().hasVersionIdPart()) {
127                        return false;
128                }
129
130                return true;
131        }
132        
133        @Override
134        public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
135                IdDt id = null;
136                String resourceName = myResourceName;
137                if (myIdParamIndex != null) {
138                        id = (IdDt) theArgs[myIdParamIndex];
139                        if (id == null || isBlank(id.getValue())) {
140                                throw new NullPointerException("ID can not be null");
141                        }
142                }
143
144                String historyId = id != null ? id.getIdPart() : null;
145                HttpGetClientInvocation retVal = createHistoryInvocation(resourceName, historyId, null, null);
146
147                if (theArgs != null) {
148                        for (int idx = 0; idx < theArgs.length; idx++) {
149                                IParameter nextParam = getParameters().get(idx);
150                                nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], retVal.getParameters(), null);
151                        }
152                }
153
154                return retVal;
155        }
156
157        @Override
158        public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
159                if (myIdParamIndex != null) {
160                        theMethodParams[myIdParamIndex] = theRequest.getId();
161                }
162
163                Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
164
165                final IBundleProvider resources = toResourceList(response);
166                
167                /*
168                 * We wrap the response so we can verify that it has the ID and version set,
169                 * as is the contract for history
170                 */
171                return new IBundleProvider() {
172                        
173                        @Override
174                        public InstantDt getPublished() {
175                                return resources.getPublished();
176                        }
177                        
178                        @Override
179                        public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
180                                List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex);
181                                int index = theFromIndex;
182                                for (IBaseResource nextResource : retVal) {
183                                        if (nextResource.getIdElement() == null || isBlank(nextResource.getIdElement().getIdPart())) {
184                                                throw new InternalErrorException("Server provided resource at index " + index + " with no ID set (using IResource#setId(IdDt))");
185                                        }
186                                        if (isBlank(nextResource.getIdElement().getVersionIdPart()) && nextResource instanceof IResource) {
187                                                IdDt versionId = (IdDt) ResourceMetadataKeyEnum.VERSION_ID.get((IResource) nextResource);
188                                                if (versionId == null || versionId.isEmpty()) {
189                                                        throw new InternalErrorException("Server provided resource at index " + index + " with no Version ID set (using IResource#setId(IdDt))");
190                                                }
191                                        }
192                                        index++;
193                                }
194                                return retVal;
195                        }
196                        
197                        @Override
198                        public int size() {
199                                return resources.size();
200                        }
201
202                        @Override
203                        public Integer preferredPageSize() {
204                                return null;
205                        }
206                };
207        }
208
209        public static HttpGetClientInvocation createHistoryInvocation(String theResourceName, String theId, IPrimitiveType<Date> theSince, Integer theLimit) {
210                StringBuilder b = new StringBuilder();
211                if (theResourceName != null) {
212                        b.append(theResourceName);
213                        if (isNotBlank(theId)) {
214                                b.append('/');
215                                b.append(theId);
216                        }
217                }
218                if (b.length() > 0) {
219                        b.append('/');
220                }
221                b.append(Constants.PARAM_HISTORY);
222
223                boolean haveParam = false;
224                if (theSince != null && !theSince.isEmpty()) {
225                        haveParam = true;
226                        b.append('?').append(Constants.PARAM_SINCE).append('=').append(theSince.getValueAsString());
227                }
228                if (theLimit != null) {
229                        b.append(haveParam ? '&' : '?');
230                        b.append(Constants.PARAM_COUNT).append('=').append(theLimit);
231                }
232
233                HttpGetClientInvocation retVal = new HttpGetClientInvocation(b.toString());
234                return retVal;
235        }
236
237        private static Class<? extends IBaseResource> toReturnType(Method theMethod, Object theProvider) {
238                if (theProvider instanceof IResourceProvider) {
239                        return ((IResourceProvider) theProvider).getResourceType();
240                }
241                History historyAnnotation = theMethod.getAnnotation(History.class);
242                Class<? extends IResource> type = historyAnnotation.type();
243                if (type != IResource.class) {
244                        return type;
245                }
246                return null;
247        }
248
249}