001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.rest.client;
014
015import static java.lang.String.*;
016import static org.apache.juneau.internal.ClassUtils.*;
017import static org.apache.juneau.internal.StringUtils.*;
018import static org.apache.juneau.internal.IOUtils.*;
019import static org.apache.juneau.internal.StringUtils.format;
020
021import java.io.*;
022import java.lang.reflect.*;
023import java.net.*;
024import java.text.*;
025import java.util.regex.*;
026
027import org.apache.http.*;
028import org.apache.http.client.*;
029import org.apache.http.util.*;
030import org.apache.juneau.parser.ParseException;
031
032/**
033 * Exception representing a <code>400+</code> HTTP response code against a remote resource.
034 */
035public final class RestCallException extends IOException {
036
037   private static final long serialVersionUID = 1L;
038
039   private int responseCode;
040   private String response, responseStatusMessage;
041   HttpResponseException e;
042   private HttpResponse httpResponse;
043
044   @SuppressWarnings("unused")
045   private String serverExceptionName, serverExceptionMessage, serverExceptionTrace;
046
047   /**
048    * Constructor.
049    *
050    * @param message The {@link MessageFormat}-style message.
051    * @param args Optional {@link MessageFormat}-style arguments.
052    */
053   public RestCallException(String message, Object...args) {
054      super(format(message, args));
055   }
056
057   /**
058    * Constructor.
059    *
060    * @param cause The cause of this exception.
061    * @param message The {@link MessageFormat}-style message.
062    * @param args Optional {@link MessageFormat}-style arguments.
063    */
064   public RestCallException(Throwable cause, String message, Object...args) {
065      this(getMessage(cause, message, null), args);
066      initCause(cause);
067   }
068
069   /**
070    * Constructor.
071    *
072    * @param e The inner cause of the exception.
073    */
074   public RestCallException(Exception e) {
075      super(e.getLocalizedMessage(), e);
076      if (e instanceof FileNotFoundException) {
077         responseCode = 404;
078      } else if (e.getMessage() != null) {
079         Pattern p = Pattern.compile("[^\\d](\\d{3})[^\\d]");
080         Matcher m = p.matcher(e.getMessage());
081         if (m.find())
082            responseCode = Integer.parseInt(m.group(1));
083      }
084      setStackTrace(e.getStackTrace());
085   }
086
087   /**
088    * Create an exception with a simple message and the status code and body of the specified response.
089    *
090    * @param msg The exception message.
091    * @param response The HTTP response object.
092    * @throws ParseException
093    * @throws IOException
094    */
095   public RestCallException(String msg, HttpResponse response) throws ParseException, IOException {
096      super(format("{0}\n{1}\nstatus=''{2}''\nResponse: \n{3}", msg, response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity(), UTF8)));
097   }
098
099   /**
100    * Constructor.
101    *
102    * @param responseCode The response code.
103    * @param responseMsg The response message.
104    * @param method The HTTP method (for message purposes).
105    * @param url The HTTP URL (for message purposes).
106    * @param response The response from the server.
107    */
108   public RestCallException(int responseCode, String responseMsg, String method, URI url, String response) {
109      super(format("HTTP method ''{0}'' call to ''{1}'' caused response code ''{2}, {3}''.\nResponse: \n{4}", method, url, responseCode, responseMsg, response));
110      this.responseCode = responseCode;
111      this.responseStatusMessage = responseMsg;
112      this.response = response;
113   }
114
115   /**
116    * Sets the server-side exception details.
117    *
118    * @param exceptionName The <code>Exception-Name:</code> header specifying the full name of the exception.
119    * @param exceptionMessage
120    *    The <code>Exception-Message:</code> header specifying the message returned by {@link Throwable#getMessage()}.
121    * @param exceptionTrace The stack trace of the exception returned by {@link Throwable#printStackTrace()}.
122    * @return This object (for method chaining).
123    */
124   protected RestCallException setServerException(Header exceptionName, Header exceptionMessage, Header exceptionTrace) {
125      if (exceptionName != null)
126         serverExceptionName = exceptionName.getValue();
127      if (exceptionMessage != null)
128         serverExceptionMessage = exceptionMessage.getValue();
129      if (exceptionTrace != null)
130         serverExceptionTrace = exceptionTrace.getValue();
131      return this;
132   }
133
134   /**
135    * Tries to reconstruct and re-throw the server-side exception.
136    *
137    * <p>
138    * The exception is based on the following HTTP response headers:
139    * <ul>
140    *    <li><code>Exception-Name:</code> - The full class name of the exception.
141    *    <li><code>Exception-Message:</code> - The message returned by {@link Throwable#getMessage()}.
142    *    <li><code>Exception-Trace:</code> - The stack trace of the exception returned by {@link Throwable#printStackTrace()}.
143    * </ul>
144    *
145    * <p>
146    * Does nothing if the server-side exception could not be reconstructed.
147    *
148    * <p>
149    * Currently only supports <code>Throwables</code> with either a public no-arg constructor
150    * or a public constructor that takes in a simple string message.
151    *
152    * @param cl The classloader to use to resolve the throwable class name.
153    * @param throwables The possible throwables.
154    * @throws Throwable If the throwable could be reconstructed.
155    */
156   protected void throwServerException(ClassLoader cl, Class<?>...throwables) throws Throwable {
157      if (serverExceptionName != null) {
158         for (Class<?> t : throwables)
159            if (t.getName().endsWith(serverExceptionName))
160               doThrow(t, serverExceptionMessage);
161         try {
162            Class<?> t = cl.loadClass(serverExceptionName);
163            if (isParentClass(RuntimeException.class, t) || isParentClass(Error.class, t))
164               doThrow(t, serverExceptionMessage);
165         } catch (ClassNotFoundException e2) { /* Ignore */ }
166      }
167   }
168
169   private void doThrow(Class<?> t, String msg) throws Throwable {
170      Constructor<?> c = null;
171      if (msg != null) {
172         c = findPublicConstructor(t, String.class);
173         if (c != null)
174            throw (Throwable)c.newInstance(msg);
175         c = findPublicConstructor(t, Object.class);
176         if (c != null)
177            throw (Throwable)c.newInstance(msg);
178      }
179      c = findPublicConstructor(t);
180      if (c != null)
181         throw (Throwable)c.newInstance();
182   }
183
184   /**
185    * Sets the HTTP response object that caused this exception.
186    *
187    * @param httpResponse The HTTP response object.
188    * @return This object (for method chaining).
189    */
190   protected RestCallException setHttpResponse(HttpResponse httpResponse) {
191      this.httpResponse = httpResponse;
192      return this;
193   }
194
195   /**
196    * Returns the HTTP response object that caused this exception.
197    *
198    * @return
199    *    The HTTP response object that caused this exception, or <jk>null</jk> if no response was created yet when the
200    *    exception was thrown.
201    */
202   public HttpResponse getHttpResponse() {
203      return this.httpResponse;
204   }
205
206   /**
207    * Returns the HTTP response status code.
208    *
209    * @return The response status code.  If a connection could not be made at all, returns <code>0</code>.
210    */
211   public int getResponseCode() {
212      return responseCode;
213   }
214
215   /**
216    * Returns the HTTP response message body text.
217    *
218    * @return The response message body text.
219    */
220   public String getResponseMessage() {
221      return response;
222   }
223
224   /**
225    * Returns the response status message as a plain string.
226    *
227    * @return The response status message.
228    */
229   public String getResponseStatusMessage() {
230      return responseStatusMessage;
231   }
232
233   /**
234    * Finds the message.
235    *
236    * @param cause The cause.
237    * @param msg The message.
238    * @param def The default value if both above are <jk>null</jk>.
239    * @return The resolved message.
240    */
241   protected static final String getMessage(Throwable cause, String msg, String def) {
242      if (msg != null)
243         return msg;
244      if (cause != null)
245         return cause.getMessage();
246      return def;
247   }
248}