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