001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client.classic;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022
023import org.apache.http.*;
024import org.apache.juneau.http.classic.header.*;
025
026/**
027 * Exception representing a <c>400+</c> HTTP response code against a remote resource or other exception.
028 *
029 * <h5 class='section'>See Also:</h5><ul>
030 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestClient">juneau-rest-client-classic</a>
031 * </ul>
032 *
033 * @serial exclude
034 */
035public class RestCallException extends HttpException {
036
037   private static final long serialVersionUID = 1L;
038
039   // HttpException has a bug involving ASCII control characters so just replace them with spaces.
040   private static String replaceControlChars(String message) {
041      message = emptyIfNull(message);
042
043      boolean needsCleaning = false;
044      for (var i = 0; i < message.length() && ! needsCleaning; i++)
045         if (message.charAt(i) < 32)
046            needsCleaning = true;
047
048      if (! needsCleaning)
049         return message;
050
051      var sb = new StringBuilder(message.length());
052      for (var i = 0; i < message.length(); i++) {
053         var c = message.charAt(i);
054         sb.append(c < 32 ? ' ' : c);
055      }
056
057      return sb.toString();
058   }
059
060   private static String format(String msg, Object...args) {
061      if (args.length == 0)
062         return replaceControlChars(msg);
063      return replaceControlChars(f(msg, args));
064   }
065
066   private final int statusCode;
067
068   private final Thrown thrown;
069
070   /**
071    * Constructor.
072    *
073    * @param statusCode The HTTP response status code.  Use <c>0</c> if no connection could be made.
074    * @param thrown The value of the <js>"Thrown"</js> header on the response.  Can be <jk>null</jk> if the header is not present.
075    * @param cause The cause of this exception.
076    * @param message The {@link String#format(String, Object...) String.format}-style message (<c>%s</c> placeholders).
077    * @param args Optional {@link String#format(String, Object...) String.format}-style arguments.
078    */
079   public RestCallException(int statusCode, Thrown thrown, Throwable cause, String message, Object...args) {
080      super(format(message, args), cause);
081      this.statusCode = statusCode;
082      this.thrown = thrown;
083   }
084
085   /**
086    * Constructor.
087    *
088    * @param response The HTTP response.  Can be <jk>null</jk> (treated as a connection failure: status code <c>0</c> with no thrown info).
089    * @param cause The cause of this exception.
090    * @param message The {@link String#format(String, Object...) String.format}-style message (<c>%s</c> placeholders).
091    * @param args Optional {@link String#format(String, Object...) String.format}-style arguments.
092    */
093   public RestCallException(RestResponse response, Throwable cause, String message, Object...args) {
094      this((response == null ? 0 : response.getStatusCode()), (response == null ? Thrown.EMPTY : response.getHeader("Thrown").as(Thrown.class).orElse(null)), cause, message, args);
095   }
096
097   /**
098    * Similar to {@link #getCause()} but searches until it finds the throwable of the specified type.
099    *
100    * @param <T> The throwable type.
101    * @param c The throwable type.
102    * @return The cause of the specified type, or <jk>null</jk> of not found.
103    */
104   public <T extends Throwable> T getCause(Class<T> c) {
105      return getThrowableCause(c, this);
106   }
107
108   /**
109    * Returns the HTTP response status code.
110    *
111    * @return The response status code.  If a connection could not be made at all, returns <c>0</c>.
112    */
113   public int getResponseCode() { return statusCode; }
114
115   /**
116    * Returns the value of the <js>"Thrown"</js> header on the response.
117    *
118    * @return The value of the <js>"Thrown"</js> header on the response, or <jk>null</jk> if not present.
119    */
120   public Thrown getThrown() { return thrown; }
121}