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