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