001package ca.uhn.fhir.rest.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2017 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.isNotBlank;
023
024import java.io.IOException;
025import java.io.Reader;
026import java.lang.reflect.Method;
027import java.lang.reflect.Modifier;
028import java.util.*;
029
030import org.hl7.fhir.instance.model.api.IBaseBundle;
031import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
032import org.hl7.fhir.instance.model.api.IBaseResource;
033import org.hl7.fhir.instance.model.api.IPrimitiveType;
034
035import ca.uhn.fhir.context.ConfigurationException;
036import ca.uhn.fhir.context.FhirContext;
037import ca.uhn.fhir.context.FhirVersionEnum;
038import ca.uhn.fhir.model.api.Bundle;
039import ca.uhn.fhir.model.api.IResource;
040import ca.uhn.fhir.model.api.Include;
041import ca.uhn.fhir.model.valueset.BundleTypeEnum;
042import ca.uhn.fhir.parser.IParser;
043import ca.uhn.fhir.rest.api.MethodOutcome;
044import ca.uhn.fhir.rest.api.RequestTypeEnum;
045import ca.uhn.fhir.rest.api.SummaryEnum;
046import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException;
047import ca.uhn.fhir.rest.server.Constants;
048import ca.uhn.fhir.rest.server.EncodingEnum;
049import ca.uhn.fhir.rest.server.IBundleProvider;
050import ca.uhn.fhir.rest.server.IRestfulServer;
051import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory;
052import ca.uhn.fhir.rest.server.RestfulServerUtils;
053import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
054import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
055import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
056import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
057import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
058import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
059import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
060import ca.uhn.fhir.util.BundleUtil;
061import ca.uhn.fhir.util.ReflectionUtil;
062import ca.uhn.fhir.util.UrlUtil;
063
064public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> {
065        protected static final Set<String> ALLOWED_PARAMS;
066        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class);
067
068        static {
069                HashSet<String> set = new HashSet<String>();
070                set.add(Constants.PARAM_FORMAT);
071                set.add(Constants.PARAM_NARRATIVE);
072                set.add(Constants.PARAM_PRETTY);
073                set.add(Constants.PARAM_SORT);
074                set.add(Constants.PARAM_SORT_ASC);
075                set.add(Constants.PARAM_SORT_DESC);
076                set.add(Constants.PARAM_COUNT);
077                set.add(Constants.PARAM_SUMMARY);
078                set.add(Constants.PARAM_ELEMENTS);
079                set.add(ResponseHighlighterInterceptor.PARAM_RAW);
080                ALLOWED_PARAMS = Collections.unmodifiableSet(set);
081        }
082
083        private MethodReturnTypeEnum myMethodReturnType;
084        private Class<?> myResourceListCollectionType;
085        private String myResourceName;
086        private Class<? extends IBaseResource> myResourceType;
087        private List<Class<? extends IBaseResource>> myPreferTypesList;
088
089        @SuppressWarnings("unchecked")
090        public BaseResourceReturningMethodBinding(Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
091                super(theMethod, theContext, theProvider);
092
093                Class<?> methodReturnType = theMethod.getReturnType();
094                if (Collection.class.isAssignableFrom(methodReturnType)) {
095
096                        myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
097                        Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
098                        if (collectionType != null) {
099                                if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) {
100                                        throw new ConfigurationException(
101                                                        "Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: " + collectionType);
102                                }
103                        }
104                        myResourceListCollectionType = collectionType;
105
106                } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) {
107                        if (Modifier.isAbstract(methodReturnType.getModifiers()) == false && theContext.getResourceDefinition((Class<? extends IBaseResource>) methodReturnType).isBundle()) {
108                                myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE;
109                        } else {
110                                myMethodReturnType = MethodReturnTypeEnum.RESOURCE;
111                        }
112                } else if (Bundle.class.isAssignableFrom(methodReturnType)) {
113                        myMethodReturnType = MethodReturnTypeEnum.BUNDLE;
114                } else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) {
115                        myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER;
116                } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) {
117                        myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME;
118                } else {
119                        throw new ConfigurationException(
120                                        "Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
121                }
122
123                if (theReturnResourceType != null) {
124                        if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) {
125                                if (Modifier.isAbstract(theReturnResourceType.getModifiers()) || Modifier.isInterface(theReturnResourceType.getModifiers())) {
126                                        // If we're returning an abstract type, that's ok
127                                } else {
128                                        myResourceType = (Class<? extends IResource>) theReturnResourceType;
129                                        myResourceName = theContext.getResourceDefinition(myResourceType).getName();
130                                }
131                        }
132                }
133                
134                myPreferTypesList = createPreferTypesList();
135        }
136
137        public MethodReturnTypeEnum getMethodReturnType() {
138                return myMethodReturnType;
139        }
140
141        @Override
142        public String getResourceName() {
143                return myResourceName;
144        }
145
146        /**
147         * If the response is a bundle, this type will be placed in the root of the bundle (can be null)
148         */
149        protected abstract BundleTypeEnum getResponseBundleType();
150
151        public abstract ReturnTypeEnum getReturnType();
152
153        @Override
154        public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) {
155                IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theResponseStatusCode, myPreferTypesList);
156
157                switch (getReturnType()) {
158                case BUNDLE: {
159
160                        Bundle dstu1bundle = null;
161                        IBaseBundle dstu2bundle = null;
162                        List<? extends IBaseResource> listOfResources = null;
163                        if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE || getContext().getVersion().getVersion() == FhirVersionEnum.DSTU1) {
164                                if (myResourceType != null) {
165                                        dstu1bundle = parser.parseBundle(myResourceType, theResponseReader);
166                                } else {
167                                        dstu1bundle = parser.parseBundle(theResponseReader);
168                                }
169                                listOfResources = dstu1bundle.toListOfResources();
170                        } else {
171                                Class<? extends IBaseResource> type = getContext().getResourceDefinition("Bundle").getImplementingClass();
172                                dstu2bundle = (IBaseBundle) parser.parseResource(type, theResponseReader);
173                                listOfResources = BundleUtil.toListOfResources(getContext(), dstu2bundle);
174                        }
175
176                        switch (getMethodReturnType()) {
177                        case BUNDLE:
178                                return dstu1bundle;
179                        case BUNDLE_RESOURCE:
180                                return dstu2bundle;
181                        case LIST_OF_RESOURCES:
182                                if (myResourceListCollectionType != null) {
183                                        for (Iterator<? extends IBaseResource> iter = listOfResources.iterator(); iter.hasNext();) {
184                                                IBaseResource next = iter.next();
185                                                if (!myResourceListCollectionType.isAssignableFrom(next.getClass())) {
186                                                        ourLog.debug("Not returning resource of type {} because it is not a subclass or instance of {}", next.getClass(), myResourceListCollectionType);
187                                                        iter.remove();
188                                                }
189                                        }
190                                }
191                                return listOfResources;
192                        case RESOURCE:
193                                //FIXME null access on dstu1bundle
194                                List<IResource> list = dstu1bundle.toListOfResources();
195                                if (list.size() == 0) {
196                                        return null;
197                                } else if (list.size() == 1) {
198                                        return list.get(0);
199                                } else {
200                                        throw new InvalidResponseException(theResponseStatusCode, "FHIR server call returned a bundle with multiple resources, but this method is only able to returns one.");
201                                }
202                        case BUNDLE_PROVIDER:
203                                throw new IllegalStateException("Return type of " + IBundleProvider.class.getSimpleName() + " is not supported in clients");
204                        default:
205                                break;
206                        }
207                        break;
208                }
209                case RESOURCE: {
210                        IBaseResource resource;
211                        if (myResourceType != null) {
212                                resource = parser.parseResource(myResourceType, theResponseReader);
213                        } else {
214                                resource = parser.parseResource(theResponseReader);
215                        }
216
217                        MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource);
218
219                        switch (getMethodReturnType()) {
220                        case BUNDLE:
221                                return Bundle.withSingleResource((IResource) resource);
222                        case LIST_OF_RESOURCES:
223                                return Collections.singletonList(resource);
224                        case RESOURCE:
225                                return resource;
226                        case BUNDLE_PROVIDER:
227                                throw new IllegalStateException("Return type of " + IBundleProvider.class.getSimpleName() + " is not supported in clients");
228                        case BUNDLE_RESOURCE:
229                                return resource;
230                        case METHOD_OUTCOME:
231                                MethodOutcome retVal = new MethodOutcome();
232                                retVal.setOperationOutcome((IBaseOperationOutcome) resource);
233                                return retVal;
234                        }
235                        break;
236                }
237                }
238
239                throw new IllegalStateException("Should not get here!");
240        }
241
242        @SuppressWarnings("unchecked")
243        private List<Class<? extends IBaseResource>> createPreferTypesList() {
244                List<Class<? extends IBaseResource>> preferTypes = null;
245                if (myResourceListCollectionType != null && IBaseResource.class.isAssignableFrom(myResourceListCollectionType)) {
246                        preferTypes = new ArrayList<Class<? extends IBaseResource>>(1);
247                        preferTypes.add((Class<? extends IBaseResource>) myResourceListCollectionType);
248//              } else if (myResourceType != null) {
249//                      preferTypes = new ArrayList<Class<? extends IBaseResource>>(1);
250//                      preferTypes.add((Class<? extends IBaseResource>) myResourceListCollectionType);
251                }
252                return preferTypes;
253        }
254
255        @Override
256        public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
257
258                final ResourceOrDstu1Bundle responseObject = doInvokeServer(theServer, theRequest);
259
260                Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest);
261                if (responseObject.getResource() != null) {
262
263                        for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
264                                IServerInterceptor next = theServer.getInterceptors().get(i);
265                                boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getResource());
266                                if (!continueProcessing) {
267                                        return null;
268                                }
269                        }
270
271                        boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
272
273                        return theRequest.getResponse().streamResponseAsResource(responseObject.getResource(), prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, null, theRequest.isRespondGzip(),
274                                        isAddContentLocationHeader());
275
276                } 
277                // Is this request coming from a browser
278                String uaHeader = theRequest.getHeader("user-agent");
279                boolean requestIsBrowser = false;
280                if (uaHeader != null && uaHeader.contains("Mozilla")) {
281                        requestIsBrowser = true;
282                }
283
284                for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
285                        IServerInterceptor next = theServer.getInterceptors().get(i);
286                        boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getDstu1Bundle());
287                        if (!continueProcessing) {
288                                ourLog.debug("Interceptor {} returned false, not continuing processing");
289                                return null;
290                        }
291                }
292
293                return theRequest.getResponse().streamResponseAsBundle(responseObject.getDstu1Bundle(), summaryMode, theRequest.isRespondGzip(), requestIsBrowser);
294        }
295
296        public ResourceOrDstu1Bundle doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) {
297                // Method params
298                Object[] params = new Object[getParameters().size()];
299                for (int i = 0; i < getParameters().size(); i++) {
300                        IParameter param = getParameters().get(i);
301                        if (param != null) {
302                                params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
303                        }
304                }
305
306                Object resultObj = invokeServer(theServer, theRequest, params);
307
308                Integer count = RestfulServerUtils.extractCountParameter(theRequest);
309
310                final ResourceOrDstu1Bundle responseObject;
311
312                switch (getReturnType()) {
313                case BUNDLE: {
314
315                        /*
316                         * Figure out the self-link for this request
317                         */
318                        String serverBase = theRequest.getServerBaseForRequest();
319                        String linkSelf;
320                        StringBuilder b = new StringBuilder();
321                        b.append(serverBase);
322                        if (isNotBlank(theRequest.getRequestPath())) {
323                                b.append('/');
324                                b.append(theRequest.getRequestPath());
325                        }
326                        // For POST the URL parameters get jumbled with the post body parameters so don't include them, they might be huge
327                        if (theRequest.getRequestType() == RequestTypeEnum.GET) {
328                                boolean first = true;
329                                Map<String, String[]> parameters = theRequest.getParameters();
330                                for (String nextParamName : new TreeSet<String>(parameters.keySet())) {
331                                        for (String nextParamValue : parameters.get(nextParamName)) {
332                                                if (first) {
333                                                        b.append('?');
334                                                        first = false;
335                                                } else {
336                                                        b.append('&');
337                                                }
338                                                b.append(UrlUtil.escape(nextParamName));
339                                                b.append('=');
340                                                b.append(UrlUtil.escape(nextParamValue));
341                                        }
342                                }
343                        }
344                        linkSelf = b.toString();
345
346                        if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE_RESOURCE) {
347                                IBaseResource resource;
348                                IPrimitiveType<Date> lastUpdated;
349                                if (resultObj instanceof IBundleProvider) {
350                                        IBundleProvider result = (IBundleProvider) resultObj;
351                                        resource = result.getResources(0, 1).get(0);
352                                        lastUpdated = result.getPublished();
353                                } else {
354                                        resource = (IBaseResource) resultObj;
355                                        lastUpdated = theServer.getFhirContext().getVersion().getLastUpdated(resource);
356                                }
357
358                                /*
359                                 * We assume that the bundle we got back from the handling method may not have everything populated (e.g. self links, bundle type, etc) so we do that here.
360                                 */
361                                IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
362                                bundleFactory.initializeWithBundleResource(resource);
363                                bundleFactory.addRootPropertiesToBundle(null, theRequest.getFhirServerBase(), linkSelf, count, getResponseBundleType(), lastUpdated);
364
365                                responseObject = new ResourceOrDstu1Bundle(resource);
366                        } else {
367                                Set<Include> includes = getRequestIncludesFromParams(params);
368
369                                IBundleProvider result = (IBundleProvider) resultObj;
370                                if (count == null) {
371                                        count = result.preferredPageSize();
372                                }
373
374                                Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET);
375                                if (offsetI == null || offsetI < 0) {
376                                        offsetI = 0;
377                                }
378                                
379                                Integer resultSize = result.size();
380                                int start;
381                                if (resultSize != null) {
382                                        start = Math.max(0, Math.min(offsetI, resultSize - 1));
383                                } else {
384                                        start = offsetI;
385                                }
386                                
387                                IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
388
389                                ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding());
390                                EncodingEnum linkEncoding = theRequest.getParameters().containsKey(Constants.PARAM_FORMAT) && responseEncoding != null ? responseEncoding.getEncoding() : null;
391
392                                boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
393                                bundleFactory.initializeBundleFromBundleProvider(theServer, result, linkEncoding, theRequest.getFhirServerBase(), linkSelf, prettyPrint, start, count, null, getResponseBundleType(),
394                                                includes);
395                                Bundle bundle = bundleFactory.getDstu1Bundle();
396                                if (bundle != null) {
397                                        responseObject = new ResourceOrDstu1Bundle(bundle);
398                                } else {
399                                        IBaseResource resBundle = bundleFactory.getResourceBundle();
400                                        responseObject = new ResourceOrDstu1Bundle(resBundle);
401                                }
402                        }
403                        break;
404                }
405                case RESOURCE: {
406                        IBundleProvider result = (IBundleProvider) resultObj;
407                        if (result.size() == 0) {
408                                throw new ResourceNotFoundException(theRequest.getId());
409                        } else if (result.size() > 1) {
410                                throw new InternalErrorException("Method returned multiple resources");
411                        }
412
413                        IBaseResource resource = result.getResources(0, 1).get(0);
414                        responseObject = new ResourceOrDstu1Bundle(resource);
415                        break;
416                }
417                default:
418                        throw new IllegalStateException(); // should not happen
419                }
420                return responseObject;
421        }
422
423        public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException;
424
425        /**
426         * Should the response include a Content-Location header. Search method bunding (and any others?) may override this to disable the content-location, since it doesn't make sense
427         */
428        protected boolean isAddContentLocationHeader() {
429                return true;
430        }
431
432        protected void setResourceName(String theResourceName) {
433                myResourceName = theResourceName;
434        }
435
436        public enum MethodReturnTypeEnum {
437                BUNDLE, BUNDLE_PROVIDER, BUNDLE_RESOURCE, LIST_OF_RESOURCES, METHOD_OUTCOME, RESOURCE
438        }
439
440        public static class ResourceOrDstu1Bundle {
441
442                private final Bundle myDstu1Bundle;
443                private final IBaseResource myResource;
444
445                public ResourceOrDstu1Bundle(Bundle theBundle) {
446                        myDstu1Bundle = theBundle;
447                        myResource = null;
448                }
449
450                public ResourceOrDstu1Bundle(IBaseResource theResource) {
451                        myResource = theResource;
452                        myDstu1Bundle = null;
453                }
454
455                public Bundle getDstu1Bundle() {
456                        return myDstu1Bundle;
457                }
458
459                public IBaseResource getResource() {
460                        return myResource;
461                }
462
463        }
464
465        public enum ReturnTypeEnum {
466                BUNDLE, RESOURCE
467        }
468
469}