001/*
002 *   Copyright 2024 Vonage
003 *
004 *   Licensed under the Apache License, Version 2.0 (the "License");
005 *   you may not use this file except in compliance with the License.
006 *   You may obtain a copy of the License at
007 *
008 *        http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *   Unless required by applicable law or agreed to in writing, software
011 *   distributed under the License is distributed on an "AS IS" BASIS,
012 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *   See the License for the specific language governing permissions and
014 *   limitations under the License.
015 */
016package com.vonage.client;
017
018import com.fasterxml.jackson.annotation.JsonIgnore;
019import com.fasterxml.jackson.annotation.JsonProperty;
020import com.fasterxml.jackson.annotation.JsonSetter;
021import com.fasterxml.jackson.core.JsonProcessingException;
022import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
023import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
024import java.net.URI;
025import java.util.List;
026import java.util.Objects;
027
028/**
029 * Base class for exceptions which represent an error API response conforming to
030 * <a href=https://www.rfc-editor.org/rfc/rfc7807>RFC 7807</a> standard.
031 */
032public class VonageApiResponseException extends VonageClientException implements Jsonable {
033        protected URI type;
034        protected String title, detail, instance, code, errorCodeLabel, errorCode;
035        protected List<?> errors, invalidParameters;
036        @JsonIgnore protected int statusCode;
037
038        protected VonageApiResponseException() {
039        }
040
041        protected VonageApiResponseException(String message) {
042                super(message);
043        }
044
045        protected VonageApiResponseException(String message, Throwable cause) {
046                super(message, cause);
047        }
048
049        protected VonageApiResponseException(Throwable cause) {
050                super(cause);
051        }
052
053        @JsonSetter("error-code")
054        private void setErrorCode(String errorCode) {
055                if (errorCode != null && !errorCode.trim().isEmpty()) {
056                        statusCode = Integer.parseInt(errorCode);
057                }
058        }
059
060        /**
061         * The status code description.
062         *
063         * @return The error code label, or {@code null} if unknown.
064         */
065        @JsonProperty("error-code-label")
066        public String getErrorCodeLabel() {
067                return errorCodeLabel;
068        }
069
070        /**
071         * Link to the <a href=https://developer.vonage.com/en/api-errors>API error type</a>.
072         *
073         * @return URL of the error type / description.
074         */
075        @JsonProperty("type")
076        public URI getType() {
077                return type;
078        }
079
080        /**
081         * Brief description / name of the error.
082         *
083         * @return The error title.
084         */
085        @JsonProperty("title")
086        public String getTitle() {
087                return title;
088        }
089
090        /**
091         * Extended description of the error, explaining the cause.
092         *
093         * @return The detailed error description.
094         */
095        @JsonProperty("detail")
096        public String getDetail() {
097                return detail;
098        }
099
100        /**
101         * Internal trace ID of the request.
102         *
103         * @return The instance ID.
104         */
105        @JsonProperty("instance")
106        public String getInstance() {
107                return instance;
108        }
109
110        /**
111         * Additional description of problems encountered with the request.
112         * This is typically only applicable to 400 or 409 error codes.
113         *
114         * @return The list of errors returned from the server (could be a Map or String),
115         * or {@code null} if none / not applicable.
116         */
117        @JsonProperty("errors")
118        public List<?> getErrors() {
119                return errors;
120        }
121
122        /**
123         * If the request was rejected due to invalid parameters, this will return the
124         * offending parameters, sometimes along with a description of the errors.
125         *
126         * @return The list of invalid parameters, typically as a Map, or {@code null} if not applicable.
127         *
128         * @since 7.7.0
129         */
130        @JsonProperty("invalid_parameters")
131        public List<?> getInvalidParameters() {
132                return invalidParameters;
133        }
134
135        /**
136         * Name of the error code.
137         *
138         * @return The Vonage error code as a string, or {@code null} if unknown / not applicable.
139         */
140        @JsonProperty("code")
141        public String getCode() {
142                return code;
143        }
144
145        /**
146         * The API response status code, usually 4xx or 500.
147         *
148         * @return The status code.
149         */
150        @JsonIgnore
151        public int getStatusCode() {
152                return statusCode;
153        }
154
155        @JsonIgnore
156        @Override
157        public String getMessage() {
158                if (statusCode > 0 && title != null) {
159                        String message = statusCode+" ("+title+")";
160                        if (detail != null) {
161                                message += ": "+detail;
162                        }
163                        return message;
164                }
165                else return super.getMessage();
166        }
167
168        private static class IgnoreInheritedIntrospector extends JacksonAnnotationIntrospector {
169                @Override
170                public boolean hasIgnoreMarker(final AnnotatedMember m) {
171                        return m.getDeclaringClass().equals(Throwable.class) || super.hasIgnoreMarker(m);
172                }
173        }
174
175        @Override
176        public boolean equals(Object o) {
177                if (this == o) return true;
178                if (o == null || getClass() != o.getClass()) return false;
179                VonageApiResponseException response = (VonageApiResponseException) o;
180                return statusCode == response.statusCode &&
181                                Objects.equals(type, response.type) &&
182                                Objects.equals(title, response.title) &&
183                                Objects.equals(detail, response.detail) &&
184                                Objects.equals(instance, response.instance);
185        }
186
187        @Override
188        public int hashCode() {
189                return Objects.hash(type, title, detail, instance, statusCode);
190        }
191
192        /**
193         * Generates JSON from this object. Excludes fields inherited from superclasses.
194         *
195         * @return The JSON representation of this response object.
196         */
197        @Override
198        public String toJson() {
199                try {
200                        return Jsonable.createDefaultObjectMapper()
201                                        .setAnnotationIntrospector(new IgnoreInheritedIntrospector())
202                                        .writeValueAsString(this);
203                }
204                catch (JsonProcessingException e) {
205                        throw new VonageUnexpectedException("Failed to produce JSON from "+getClass().getSimpleName(), e);
206                }
207        }
208
209        protected static <E extends VonageApiResponseException> E fromJson(Class<E> clazz, String json) {
210                if (json == null || json.length() < 2) {
211                        try {
212                                return clazz.getConstructor().newInstance();
213                        }
214                        catch (Exception ex) {
215                                throw new VonageUnexpectedException(ex);
216                        }
217                }
218                else return Jsonable.fromJson(json, clazz);
219        }
220}