001package ca.uhn.fhir.jaxrs.server;
002
003/*
004 * #%L
005 * HAPI FHIR JAX-RS Server
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 */
022
023import java.io.IOException;
024import java.lang.annotation.Annotation;
025import java.lang.reflect.Method;
026import java.lang.reflect.Modifier;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Map.Entry;
032import java.util.Set;
033import java.util.concurrent.ConcurrentHashMap;
034
035import javax.annotation.PostConstruct;
036import javax.ws.rs.GET;
037import javax.ws.rs.OPTIONS;
038import javax.ws.rs.Path;
039import javax.ws.rs.Produces;
040import javax.ws.rs.core.MediaType;
041import javax.ws.rs.core.Response;
042
043import org.apache.commons.lang3.StringUtils;
044import org.hl7.fhir.dstu3.hapi.rest.server.ServerConformanceProvider;
045import org.hl7.fhir.dstu3.model.Conformance;
046import org.hl7.fhir.instance.model.api.IBaseResource;
047import org.slf4j.LoggerFactory;
048
049import ca.uhn.fhir.context.ConfigurationException;
050import ca.uhn.fhir.context.FhirContext;
051import ca.uhn.fhir.context.FhirVersionEnum;
052import ca.uhn.fhir.context.RuntimeResourceDefinition;
053import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder;
054import ca.uhn.fhir.rest.annotation.IdParam;
055import ca.uhn.fhir.rest.api.RequestTypeEnum;
056import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
057import ca.uhn.fhir.rest.api.SummaryEnum;
058import ca.uhn.fhir.rest.method.BaseMethodBinding;
059import ca.uhn.fhir.rest.method.ParseAction;
060import ca.uhn.fhir.rest.server.Constants;
061import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy;
062import ca.uhn.fhir.rest.server.IResourceProvider;
063import ca.uhn.fhir.rest.server.IRestfulResponse;
064import ca.uhn.fhir.rest.server.ResourceBinding;
065import ca.uhn.fhir.rest.server.RestulfulServerConfiguration;
066import ca.uhn.fhir.util.ReflectionUtil;
067
068/**
069 * This is the conformance provider for the jax rs servers. It requires all providers to be registered during startup because the conformance profile is generated during the postconstruct phase.
070 * 
071 * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
072 */
073@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
074public abstract class AbstractJaxRsConformanceProvider extends AbstractJaxRsProvider implements IResourceProvider {
075
076        /** the logger */
077        private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(AbstractJaxRsConformanceProvider.class);
078        /** the server bindings */
079        private ResourceBinding myServerBinding = new ResourceBinding();
080        /** the resource bindings */
081        private ConcurrentHashMap<String, ResourceBinding> myResourceNameToBinding = new ConcurrentHashMap<String, ResourceBinding>();
082        /** the server configuration */
083        private RestulfulServerConfiguration serverConfiguration = new RestulfulServerConfiguration();
084
085        /** the conformance. It is created once during startup */
086        private Conformance myDstu3Conformance;
087        private ca.uhn.fhir.model.dstu2.resource.Conformance myDstu2Conformance;
088
089        /**
090         * Constructor allowing the description, servername and server to be set
091         * 
092         * @param implementationDescription
093         *           the implementation description. If null, "" is used
094         * @param serverName
095         *           the server name. If null, "" is used
096         * @param serverVersion
097         *           the server version. If null, "" is used
098         */
099        protected AbstractJaxRsConformanceProvider(String implementationDescription, String serverName, String serverVersion) {
100                serverConfiguration.setFhirContext(getFhirContext());
101                serverConfiguration.setImplementationDescription(StringUtils.defaultIfEmpty(implementationDescription, ""));
102                serverConfiguration.setServerName(StringUtils.defaultIfEmpty(serverName, ""));
103                serverConfiguration.setServerVersion(StringUtils.defaultIfEmpty(serverVersion, ""));
104        }
105
106        /**
107         * Constructor allowing the description, servername and server to be set
108         * 
109         * @param ctx
110         *           the {@link FhirContext} instance.
111         * @param implementationDescription
112         *           the implementation description. If null, "" is used
113         * @param serverName
114         *           the server name. If null, "" is used
115         * @param serverVersion
116         *           the server version. If null, "" is used
117         */
118        protected AbstractJaxRsConformanceProvider(FhirContext ctx, String implementationDescription, String serverName, String serverVersion) {
119                super(ctx);
120                serverConfiguration.setFhirContext(ctx);
121                serverConfiguration.setImplementationDescription(StringUtils.defaultIfEmpty(implementationDescription, ""));
122                serverConfiguration.setServerName(StringUtils.defaultIfEmpty(serverName, ""));
123                serverConfiguration.setServerVersion(StringUtils.defaultIfEmpty(serverVersion, ""));
124        }
125
126        /**
127         * This method will set the conformance during the postconstruct phase. The method {@link AbstractJaxRsConformanceProvider#getProviders()} is used to get all the resource providers include in the
128         * conformance
129         */
130        @PostConstruct
131        protected void setUpPostConstruct() {
132                for (Entry<Class<? extends IResourceProvider>, IResourceProvider> provider : getProviders().entrySet()) {
133                        addProvider(provider.getValue(), provider.getKey());
134                }
135                List<BaseMethodBinding<?>> serverBindings = new ArrayList<BaseMethodBinding<?>>();
136                for (ResourceBinding baseMethodBinding : myResourceNameToBinding.values()) {
137                        serverBindings.addAll(baseMethodBinding.getMethodBindings());
138                }
139                serverConfiguration.setServerBindings(serverBindings);
140                serverConfiguration.setResourceBindings(new LinkedList<ResourceBinding>(myResourceNameToBinding.values()));
141                HardcodedServerAddressStrategy hardcodedServerAddressStrategy = new HardcodedServerAddressStrategy();
142                hardcodedServerAddressStrategy.setValue(getBaseForServer());
143                serverConfiguration.setServerAddressStrategy(hardcodedServerAddressStrategy);
144                if (super.getFhirContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
145                        ServerConformanceProvider serverConformanceProvider = new ServerConformanceProvider(serverConfiguration);
146                        serverConformanceProvider.initializeOperations();
147                        myDstu3Conformance = serverConformanceProvider.getServerConformance(null);
148                } else if (super.getFhirContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU2)) {
149                        ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider serverConformanceProvider = new ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider(serverConfiguration);
150                        serverConformanceProvider.initializeOperations();
151                        myDstu2Conformance = serverConformanceProvider.getServerConformance(null);
152                }
153        }
154
155        /**
156         * This method must return all the resource providers which need to be included in the conformance
157         * 
158         * @return a map of the resource providers and their corresponding classes. This class needs to be given explicitly because retrieving the interface using {@link Object#getClass()} may not give the
159         *         correct interface in a jee environment.
160         */
161        protected abstract ConcurrentHashMap<Class<? extends IResourceProvider>, IResourceProvider> getProviders();
162
163        /**
164         * This method will retrieve the conformance using the http OPTIONS method
165         * 
166         * @return the response containing the conformance
167         */
168        @OPTIONS
169        @Path("/metadata")
170        public Response conformanceUsingOptions() throws IOException {
171                return conformance();
172        }
173
174        /**
175         * This method will retrieve the conformance using the http GET method
176         * 
177         * @return the response containing the conformance
178         */
179        @GET
180        @Path("/metadata")
181        public Response conformance() throws IOException {
182                Builder request = getRequest(RequestTypeEnum.OPTIONS, RestOperationTypeEnum.METADATA);
183                IRestfulResponse response = request.build().getResponse();
184                response.addHeader(Constants.HEADER_CORS_ALLOW_ORIGIN, "*");
185                
186                IBaseResource conformance = null;
187                if (super.getFhirContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
188                        conformance = myDstu3Conformance;
189//                      return (Response) response.returnResponse(ParseAction.create(myDstu3Conformance), Constants.STATUS_HTTP_200_OK, true, null, getResourceType().getSimpleName());
190                } else if (super.getFhirContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU2)) {
191                        conformance = myDstu2Conformance;
192//                      return (Response) response.returnResponse(ParseAction.create(myDstu2Conformance), Constants.STATUS_HTTP_200_OK, true, null, getResourceType().getSimpleName());
193                }
194                
195                if (conformance != null) {
196                        Set<SummaryEnum> summaryMode = Collections.emptySet();
197                        return (Response) response.streamResponseAsResource(conformance, false, summaryMode, Constants.STATUS_HTTP_200_OK, null, true, false);
198                }
199                return (Response) response.returnResponse(null, Constants.STATUS_HTTP_500_INTERNAL_ERROR, true, null, getResourceType().getSimpleName());
200        }
201
202        /**
203         * This method will add a provider to the conformance. This method is almost an exact copy of {@link ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods }
204         * 
205         * @param theProvider
206         *           an instance of the provider interface
207         * @param theProviderInterface
208         *           the class describing the providers interface
209         * @return the numbers of basemethodbindings added
210         * @see ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods
211         */
212        public int addProvider(IResourceProvider theProvider, Class<? extends IResourceProvider> theProviderInterface) throws ConfigurationException {
213                int count = 0;
214
215                for (Method m : ReflectionUtil.getDeclaredMethods(theProviderInterface)) {
216                        BaseMethodBinding<?> foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider);
217                        if (foundMethodBinding == null) {
218                                continue;
219                        }
220
221                        count++;
222
223                        // if (foundMethodBinding instanceof ConformanceMethodBinding) {
224                        // myServerConformanceMethod = foundMethodBinding;
225                        // continue;
226                        // }
227
228                        if (!Modifier.isPublic(m.getModifiers())) {
229                                throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public");
230                        } else {
231                                if (Modifier.isStatic(m.getModifiers())) {
232                                        throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static");
233                                } else {
234                                        ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName());
235
236                                        String resourceName = foundMethodBinding.getResourceName();
237                                        ResourceBinding resourceBinding;
238                                        if (resourceName == null) {
239                                                resourceBinding = myServerBinding;
240                                        } else {
241                                                RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName);
242                                                if (myResourceNameToBinding.containsKey(definition.getName())) {
243                                                        resourceBinding = myResourceNameToBinding.get(definition.getName());
244                                                } else {
245                                                        resourceBinding = new ResourceBinding();
246                                                        resourceBinding.setResourceName(resourceName);
247                                                        myResourceNameToBinding.put(resourceName, resourceBinding);
248                                                }
249                                        }
250
251                                        List<Class<?>> allowableParams = foundMethodBinding.getAllowableParamAnnotations();
252                                        if (allowableParams != null) {
253                                                for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) {
254                                                        for (Annotation annotation : nextParamAnnotations) {
255                                                                Package pack = annotation.annotationType().getPackage();
256                                                                if (pack.equals(IdParam.class.getPackage())) {
257                                                                        if (!allowableParams.contains(annotation.annotationType())) {
258                                                                                throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation);
259                                                                        }
260                                                                }
261                                                        }
262                                                }
263                                        }
264
265                                        resourceBinding.addMethod(foundMethodBinding);
266                                        ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName());
267                                }
268                        }
269                }
270
271                return count;
272        }
273
274        @SuppressWarnings("unchecked")
275        @Override
276        public Class<IBaseResource> getResourceType() {
277                if (super.getFhirContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
278                        return Class.class.cast(Conformance.class);
279                } else if (super.getFhirContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU2)) {
280                        return Class.class.cast(ca.uhn.fhir.model.dstu2.resource.Conformance.class);
281                }
282                return null;
283        }
284
285}