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