001package ca.uhn.fhir.rest.server.exceptions;
002
003import ca.uhn.fhir.i18n.Msg;
004import org.apache.commons.lang3.Validate;
005import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
006
007import java.lang.reflect.InvocationTargetException;
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013
014
015/*
016 * #%L
017 * HAPI FHIR - Core Library
018 * %%
019 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
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 IResourceProvider 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/hapifhir/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(PayloadTooLargeException.STATUS_CODE, PayloadTooLargeException.class);
058                registerExceptionType(AuthenticationException.STATUS_CODE, AuthenticationException.class);
059                registerExceptionType(InternalErrorException.STATUS_CODE, InternalErrorException.class);
060                registerExceptionType(InvalidRequestException.STATUS_CODE, InvalidRequestException.class);
061                registerExceptionType(MethodNotAllowedException.STATUS_CODE, MethodNotAllowedException.class);
062                registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class);
063                registerExceptionType(NotModifiedException.STATUS_CODE, NotModifiedException.class);
064                registerExceptionType(ResourceNotFoundException.STATUS_CODE, ResourceNotFoundException.class);
065                registerExceptionType(ResourceGoneException.STATUS_CODE, ResourceGoneException.class);
066                registerExceptionType(PreconditionFailedException.STATUS_CODE, PreconditionFailedException.class);
067                registerExceptionType(ResourceVersionConflictException.STATUS_CODE, ResourceVersionConflictException.class);
068                registerExceptionType(UnprocessableEntityException.STATUS_CODE, UnprocessableEntityException.class);
069                registerExceptionType(ForbiddenOperationException.STATUS_CODE, ForbiddenOperationException.class);
070        }
071
072        private List<String> myAdditionalMessages = null;
073        private IBaseOperationOutcome myBaseOperationOutcome;
074        private String myResponseBody;
075        private Map<String, List<String>> myResponseHeaders;
076        private String myResponseMimeType;
077        private int myStatusCode;
078        private boolean myErrorMessageTrusted;
079
080        /**
081         * Constructor
082         *
083         * @param theStatusCode The HTTP status code corresponding to this problem
084         * @param theMessage    The message
085         */
086        public  /**
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 = Arrays.asList(Arrays.copyOfRange(theMessages, 1, theMessages.length, String[].class));
107                }
108        }
109
110        /**
111         * Constructor
112         *
113         * @param theStatusCode           The HTTP status code corresponding to this problem
114         * @param theMessage              The message
115         * @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)
116         */
117        public BaseServerResponseException(int theStatusCode, String theMessage, IBaseOperationOutcome theBaseOperationOutcome) {
118                super(theMessage);
119                myStatusCode = theStatusCode;
120                myBaseOperationOutcome = theBaseOperationOutcome;
121        }
122
123        /**
124         * Constructor
125         *
126         * @param theStatusCode The HTTP status code corresponding to this problem
127         * @param theMessage    The message
128         * @param theCause      The cause
129         */
130        public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause) {
131                super(theMessage, theCause);
132                myStatusCode = theStatusCode;
133                myBaseOperationOutcome = null;
134        }
135
136        /**
137         * Constructor
138         *
139         * @param theStatusCode           The HTTP status code corresponding to this problem
140         * @param theMessage              The message
141         * @param theCause                The underlying cause exception
142         * @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)
143         */
144        public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause, IBaseOperationOutcome theBaseOperationOutcome) {
145                super(theMessage, theCause);
146                myStatusCode = theStatusCode;
147                myBaseOperationOutcome = theBaseOperationOutcome;
148        }
149
150        /**
151         * Constructor
152         *
153         * @param theStatusCode The HTTP status code corresponding to this problem
154         * @param theCause      The underlying cause exception
155         */
156        public BaseServerResponseException(int theStatusCode, Throwable theCause) {
157                super(theCause.getMessage(), theCause);
158                myStatusCode = theStatusCode;
159                myBaseOperationOutcome = null;
160        }
161
162        /**
163         * Constructor
164         *
165         * @param theStatusCode           The HTTP status code corresponding to this problem
166         * @param theCause                The underlying cause exception
167         * @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)
168         */
169        public BaseServerResponseException(int theStatusCode, Throwable theCause, IBaseOperationOutcome theBaseOperationOutcome) {
170                super(theCause.toString(), theCause);
171                myStatusCode = theStatusCode;
172                myBaseOperationOutcome = theBaseOperationOutcome;
173        }
174
175        /**
176         * This flag can be used to signal to server infrastructure that the message supplied
177         * to this exception (ie to the constructor) is considered trusted and is safe to
178         * return to the calling client.
179         */
180        public boolean isErrorMessageTrusted() {
181                return myErrorMessageTrusted;
182        }
183
184        /**
185         * This flag can be used to signal to server infrastructure that the message supplied
186         * to this exception (ie to the constructor) is considered trusted and is safe to
187         * return to the calling client.
188         */
189        public BaseServerResponseException setErrorMessageTrusted(boolean theErrorMessageTrusted) {
190                myErrorMessageTrusted = theErrorMessageTrusted;
191                return this;
192        }
193
194        /**
195         * Add a header which will be added to any responses
196         *
197         * @param theName  The header name
198         * @param theValue The header value
199         * @return Returns a reference to <code>this</code> for easy method chaining
200         * @since 2.0
201         */
202        public BaseServerResponseException addResponseHeader(String theName, String theValue) {
203                Validate.notBlank(theName, "theName must not be null or empty");
204                Validate.notBlank(theValue, "theValue must not be null or empty");
205                if (getResponseHeaders().containsKey(theName) == false) {
206                        getResponseHeaders().put(theName, new ArrayList<>());
207                }
208                getResponseHeaders().get(theName).add(theValue);
209                return this;
210        }
211
212        public List<String> getAdditionalMessages() {
213                return myAdditionalMessages;
214        }
215
216        /**
217         * Returns the {@link IBaseOperationOutcome} resource if any which was supplied in the response, or <code>null</code>
218         */
219        public IBaseOperationOutcome getOperationOutcome() {
220                return myBaseOperationOutcome;
221        }
222
223        /**
224         * Sets the BaseOperationOutcome resource associated with this exception. In server implementations, this is the OperartionOutcome resource to include with the HTTP response. In client
225         * implementations you should not call this method.
226         *
227         * @param theBaseOperationOutcome The BaseOperationOutcome resource Sets the BaseOperationOutcome resource associated with this exception. In server implementations, this is the OperartionOutcome resource to include
228         *                                with the HTTP response. In client implementations you should not call this method.
229         */
230        public void setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) {
231                myBaseOperationOutcome = theBaseOperationOutcome;
232        }
233
234        /**
235         * 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.
236         * <p>
237         * In a restful server, this method is currently ignored.
238         * </p>
239         */
240        public String getResponseBody() {
241                return myResponseBody;
242        }
243
244        /**
245         * This method is currently only called internally by HAPI, it should not be called by user code.
246         */
247        public void setResponseBody(String theResponseBody) {
248                myResponseBody = theResponseBody;
249        }
250
251        /**
252         * Returns a map containing any headers which should be added to the outgoing
253         * response. This methos creates the map if none exists, so it will never
254         * return <code>null</code>
255         *
256         * @since 2.0 (note that this method existed in previous versions of HAPI but the method
257         * signature has been changed from <code>Map&lt;String, String[]&gt;</code> to <code>Map&lt;String, List&lt;String&gt;&gt;</code>
258         */
259        public Map<String, List<String>> getResponseHeaders() {
260                if (myResponseHeaders == null) {
261                        myResponseHeaders = new HashMap<>();
262                }
263                return myResponseHeaders;
264        }
265
266        /**
267         * In a RESTful client, this method will be populated with the HTTP status code that was returned with the HTTP response.
268         * <p>
269         * In a restful server, this method is currently ignored.
270         * </p>
271         */
272        public String getResponseMimeType() {
273                return myResponseMimeType;
274        }
275
276        /**
277         * This method is currently only called internally by HAPI, it should not be called by user code.
278         */
279        public void setResponseMimeType(String theResponseMimeType) {
280                myResponseMimeType = theResponseMimeType;
281        }
282
283        /**
284         * Returns the HTTP status code corresponding to this problem
285         */
286        public int getStatusCode() {
287                return myStatusCode;
288        }
289
290        /**
291         * Does the exception have any headers which should be added to the outgoing response?
292         *
293         * @see #getResponseHeaders()
294         * @since 2.0
295         */
296        public boolean hasResponseHeaders() {
297                return myResponseHeaders != null && myResponseHeaders.isEmpty() == false;
298        }
299
300        /**
301         * For unit tests only
302         */
303        static boolean isExceptionTypeRegistered(Class<?> theType) {
304                return ourStatusCodeToExceptionType.values().contains(theType);
305        }
306
307        public static BaseServerResponseException newInstance(int theStatusCode, String theMessage) {
308                if (ourStatusCodeToExceptionType.containsKey(theStatusCode)) {
309                        try {
310                                return ourStatusCodeToExceptionType.get(theStatusCode).getConstructor(new Class[]{String.class}).newInstance(theMessage);
311                        } catch (InstantiationException e) {
312                                throw new InternalErrorException(Msg.code(1912) + e);
313                        } catch (IllegalAccessException e) {
314                                throw new InternalErrorException(Msg.code(1913) + e);
315                        } catch (IllegalArgumentException e) {
316                                throw new InternalErrorException(Msg.code(1914) + e);
317                        } catch (InvocationTargetException e) {
318                                throw new InternalErrorException(Msg.code(1915) + e);
319                        } catch (NoSuchMethodException e) {
320                                throw new InternalErrorException(Msg.code(1916) + e);
321                        } catch (SecurityException e) {
322                                throw new InternalErrorException(Msg.code(1917) + e);
323                        }
324                }
325                return new UnclassifiedServerFailureException(theStatusCode, theMessage);
326        }
327
328        static void registerExceptionType(int theStatusCode, Class<? extends BaseServerResponseException> theType) {
329                if (ourStatusCodeToExceptionType.containsKey(theStatusCode)) {
330                        throw new Error(Msg.code(1918) + "Can not register " + theType + " to status code " + theStatusCode + " because " + ourStatusCodeToExceptionType.get(theStatusCode) + " already registers that code");
331                }
332                ourStatusCodeToExceptionType.put(theStatusCode, theType);
333        }
334
335}