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