001package ca.uhn.fhir.rest.server.exceptions;
002
003import java.lang.reflect.InvocationTargetException;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009
010import org.apache.commons.lang3.Validate;
011import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
012
013import ca.uhn.fhir.rest.server.IResourceProvider;
014
015/*
016 * #%L
017 * HAPI FHIR - Core Library
018 * %%
019 * Copyright (C) 2014 - 2017 University Health Network
020 * %%
021 * Licensed under the Apache License, Version 2.0 (the "License");
022 * you may not use this file except in compliance with the License.
023 * You may obtain a copy of the License at
024 * 
025 *      http://www.apache.org/licenses/LICENSE-2.0
026 * 
027 * Unless required by applicable law or agreed to in writing, software
028 * distributed under the License is distributed on an "AS IS" BASIS,
029 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
030 * See the License for the specific language governing permissions and
031 * limitations under the License.
032 * #L%
033 */
034
035/**
036 * Base class for RESTful client and server exceptions. RESTful client methods will only throw exceptions which are subclasses of this exception type, and RESTful server methods should also only call
037 * subclasses of this exception type.
038 * <p>
039 * HAPI provides a number of subclasses of BaseServerResponseException, and each one corresponds to a specific
040 * HTTP status code. For example, if a {@link IResourceProvider resource provider} method throws 
041 * {@link ResourceNotFoundException}, this is a signal to the server that an <code>HTTP 404</code> should
042 * be returned to the client.
043 * </p>
044 * <p>
045 * <b>See:</b> A complete list of available exceptions is in the <a href="./package-summary.html">package summary</a>.
046 * If an exception doesn't exist for a condition you want to represent, let us know by filing an
047 * <a href="https://github.com/jamesagnew/hapi-fhir/issues">issue in our tracker</a>. You may also
048 * use {@link UnclassifiedServerFailureException} to represent any error code you want.
049 * </p>
050 */
051public abstract class BaseServerResponseException extends RuntimeException {
052
053        private static final Map<Integer, Class<? extends BaseServerResponseException>> ourStatusCodeToExceptionType = new HashMap<Integer, Class<? extends BaseServerResponseException>>();
054        private static final long serialVersionUID = 1L;
055
056        static {
057                registerExceptionType(AuthenticationException.STATUS_CODE, AuthenticationException.class);
058                registerExceptionType(InternalErrorException.STATUS_CODE, InternalErrorException.class);
059                registerExceptionType(InvalidRequestException.STATUS_CODE, InvalidRequestException.class);
060                registerExceptionType(MethodNotAllowedException.STATUS_CODE, MethodNotAllowedException.class);
061                registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class);
062                registerExceptionType(NotModifiedException.STATUS_CODE, NotModifiedException.class);
063                registerExceptionType(ResourceNotFoundException.STATUS_CODE, ResourceNotFoundException.class);
064                registerExceptionType(ResourceGoneException.STATUS_CODE, ResourceGoneException.class);
065                registerExceptionType(PreconditionFailedException.STATUS_CODE, PreconditionFailedException.class);
066                registerExceptionType(ResourceVersionConflictException.STATUS_CODE, ResourceVersionConflictException.class);
067                registerExceptionType(UnprocessableEntityException.STATUS_CODE, UnprocessableEntityException.class);
068                registerExceptionType(ForbiddenOperationException.STATUS_CODE, ForbiddenOperationException.class);
069        }
070
071        private List<String> myAdditionalMessages = null;
072        private IBaseOperationOutcome myBaseOperationOutcome;
073        private String myResponseBody;
074        private Map<String, List<String>> myResponseHeaders;
075        private String myResponseMimeType;
076        private int myStatusCode;
077
078        /**
079         * Constructor
080         * 
081         * @param theStatusCode
082         *            The HTTP status code corresponding to this problem
083         * @param theMessage
084         *            The message
085         */
086        public BaseServerResponseException(int theStatusCode, String theMessage) {
087                super(theMessage);
088                myStatusCode = theStatusCode;
089                myBaseOperationOutcome = null;
090        }
091        
092        /**
093         * Constructor
094         * 
095         * @param theStatusCode
096         *            The HTTP status code corresponding to this problem
097         * @param theMessages
098         *            The messages
099         */
100        public BaseServerResponseException(int theStatusCode, String... theMessages) {
101                super(theMessages != null && theMessages.length > 0 ? theMessages[0] : null);
102                myStatusCode = theStatusCode;
103                myBaseOperationOutcome = null;
104                if (theMessages != null && theMessages.length > 1) {
105                        myAdditionalMessages = Arrays.asList(Arrays.copyOfRange(theMessages, 1, theMessages.length, String[].class));
106                }
107        }
108        
109        /**
110         * Constructor
111         * 
112         * @param theStatusCode
113         *            The HTTP status code corresponding to this problem
114         * @param theMessage
115         *            The message
116         * @param theBaseOperationOutcome
117         *            An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
118         */
119        public BaseServerResponseException(int theStatusCode, String theMessage, IBaseOperationOutcome theBaseOperationOutcome) {
120                super(theMessage);
121                myStatusCode = theStatusCode;
122                myBaseOperationOutcome = theBaseOperationOutcome;
123        }
124
125        /**
126         * Constructor
127         * 
128         * @param theStatusCode
129         *            The HTTP status code corresponding to this problem
130         * @param theMessage
131         *            The message
132         * @param theCause
133         *            The cause
134         */
135        public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause) {
136                super(theMessage, theCause);
137                myStatusCode = theStatusCode;
138                myBaseOperationOutcome = null;
139        }
140
141        /**
142         * Constructor
143         * 
144         * @param theStatusCode
145         *            The HTTP status code corresponding to this problem
146         * @param theMessage
147         *            The message
148         * @param theCause
149         *            The underlying cause exception
150         * @param theBaseOperationOutcome
151         *            An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
152         */
153        public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause, IBaseOperationOutcome theBaseOperationOutcome) {
154                super(theMessage, theCause);
155                myStatusCode = theStatusCode;
156                myBaseOperationOutcome = theBaseOperationOutcome;
157        }
158
159        /**
160         * Constructor
161         * 
162         * @param theStatusCode
163         *            The HTTP status code corresponding to this problem
164         * @param theCause
165         *            The underlying cause exception
166         */
167        public BaseServerResponseException(int theStatusCode, Throwable theCause) {
168                super(theCause.toString(), theCause);
169                myStatusCode = theStatusCode;
170                myBaseOperationOutcome = null;
171        }
172
173        /**
174         * Constructor
175         * 
176         * @param theStatusCode
177         *            The HTTP status code corresponding to this problem
178         * @param theCause
179         *            The underlying cause exception
180         * @param theBaseOperationOutcome
181         *            An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
182         */
183        public BaseServerResponseException(int theStatusCode, Throwable theCause, IBaseOperationOutcome theBaseOperationOutcome) {
184                super(theCause.toString(), theCause);
185                myStatusCode = theStatusCode;
186                myBaseOperationOutcome = theBaseOperationOutcome;
187        }
188
189        /**
190         * Add a header which will be added to any responses
191         * 
192         * @param theName The header name
193         * @param theValue The header value
194         * @return Returns a reference to <code>this</code> for easy method chaining
195         * @since 2.0
196         */
197        public BaseServerResponseException addResponseHeader(String theName, String theValue) {
198                Validate.notBlank(theName, "theName must not be null or empty");
199                Validate.notBlank(theValue, "theValue must not be null or empty");
200                if (getResponseHeaders().containsKey(theName) == false) {
201                        getResponseHeaders().put(theName, new ArrayList<String>());
202                }
203                getResponseHeaders().get(theName).add(theValue);
204                return this;
205        }
206
207        public List<String> getAdditionalMessages() {
208                return myAdditionalMessages;
209        }
210
211        /**
212         * Returns the {@link IBaseOperationOutcome} resource if any which was supplied in the response, or <code>null</code>
213         */
214        public IBaseOperationOutcome getOperationOutcome() {
215                return myBaseOperationOutcome;
216        }
217
218        /**
219         * In a RESTful client, this method will be populated with the body of the HTTP respone if one was provided by the server, or <code>null</code> otherwise.
220         * <p>
221         * In a restful server, this method is currently ignored.
222         * </p>
223         */
224        public String getResponseBody() {
225                return myResponseBody;
226        }
227
228        /**
229         * Returns a map containing any headers which should be added to the outgoing
230         * response. This methos creates the map if none exists, so it will never
231         * return <code>null</code>
232         * 
233         * @since 2.0 (note that this method existed in previous versions of HAPI but the method
234         * signature has been changed from <code>Map&lt;String, String[]&gt;</code> to <code>Map&lt;String, List&lt;String&gt;&gt;</code> 
235         */
236        public Map<String, List<String>> getResponseHeaders() {
237                if (myResponseHeaders == null) {
238                        myResponseHeaders = new HashMap<String, List<String>>();
239                }
240                return myResponseHeaders;
241        }
242
243        /**
244         * In a RESTful client, this method will be populated with the HTTP status code that was returned with the HTTP response.
245         * <p>
246         * In a restful server, this method is currently ignored.
247         * </p>
248         */
249        public String getResponseMimeType() {
250                return myResponseMimeType;
251        }
252
253        /**
254         * Returns the HTTP status code corresponding to this problem
255         */
256        public int getStatusCode() {
257                return myStatusCode;
258        }
259
260        /**
261         * Does the exception have any headers which should be added to the outgoing response?
262         * 
263         * @see #getResponseHeaders()
264         * @since 2.0
265         */
266        public boolean hasResponseHeaders() {
267                return myResponseHeaders != null && myResponseHeaders.isEmpty() == false;
268        }
269
270        /**
271         * Sets the BaseOperationOutcome resource associated with this exception. In server implementations, this is the OperartionOutcome resource to include with the HTTP response. In client
272         * implementations you should not call this method.
273         * 
274         * @param theBaseOperationOutcome
275         *            The BaseOperationOutcome resource Sets the BaseOperationOutcome resource associated with this exception. In server implementations, this is the OperartionOutcome resource to include
276         *            with the HTTP response. In client implementations you should not call this method.
277         */
278        public void setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) {
279                myBaseOperationOutcome = theBaseOperationOutcome;
280        }
281
282        /**
283         * This method is currently only called internally by HAPI, it should not be called by user code.
284         */
285        public void setResponseBody(String theResponseBody) {
286                myResponseBody = theResponseBody;
287        }
288
289        /**
290         * This method is currently only called internally by HAPI, it should not be called by user code.
291         */
292        public void setResponseMimeType(String theResponseMimeType) {
293                myResponseMimeType = theResponseMimeType;
294        }
295
296        /**
297         * For unit tests only
298         */
299        static boolean isExceptionTypeRegistered(Class<?> theType) {
300                return ourStatusCodeToExceptionType.values().contains(theType);
301        }
302
303        public static BaseServerResponseException newInstance(int theStatusCode, String theMessage) {
304                if (ourStatusCodeToExceptionType.containsKey(theStatusCode)) {
305                        try {
306                                return ourStatusCodeToExceptionType.get(theStatusCode).getConstructor(new Class[] { String.class }).newInstance(theMessage);
307                        } catch (InstantiationException e) {
308                                throw new InternalErrorException(e);
309                        } catch (IllegalAccessException e) {
310                                throw new InternalErrorException(e);
311                        } catch (IllegalArgumentException e) {
312                                throw new InternalErrorException(e);
313                        } catch (InvocationTargetException e) {
314                                throw new InternalErrorException(e);
315                        } catch (NoSuchMethodException e) {
316                                throw new InternalErrorException(e);
317                        } catch (SecurityException e) {
318                                throw new InternalErrorException(e);
319                        }
320                }
321                return new UnclassifiedServerFailureException(theStatusCode, theMessage);
322        }
323
324        static void registerExceptionType(int theStatusCode, Class<? extends BaseServerResponseException> theType) {
325                if (ourStatusCodeToExceptionType.containsKey(theStatusCode)) {
326                        throw new Error("Can not register " + theType + " to status code " + theStatusCode + " because " + ourStatusCodeToExceptionType.get(theStatusCode) + " already registers that code");
327                }
328                ourStatusCodeToExceptionType.put(theStatusCode, theType);
329        }
330
331}