content: /*
   * Copyright 1999-2018 Alibaba Group Holding Ltd.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.alibaba.nacos.common.http;
  
  import com.alibaba.nacos.common.constant.HttpHeaderConsts;
  import com.alibaba.nacos.common.http.param.Header;
  import com.alibaba.nacos.common.http.param.MediaType;
  import com.alibaba.nacos.common.http.param.Query;
  import com.alibaba.nacos.common.utils.JacksonUtils;
  import com.alibaba.nacos.common.utils.StringUtils;
  import com.alibaba.nacos.common.utils.UuidUtils;
  import com.alibaba.nacos.common.utils.VersionUtils;
  import org.apache.hc.client5.http.ConnectTimeoutException;
  import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
  import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
  import org.apache.hc.core5.http.ClassicHttpRequest;
  import org.apache.hc.core5.http.ContentType;
  import org.apache.hc.core5.http.HttpEntity;
  import org.apache.hc.core5.http.HttpEntityContainer;
  import org.apache.hc.core5.http.NameValuePair;
  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
  import org.apache.hc.core5.http.io.entity.StringEntity;
  import org.apache.hc.core5.http.message.BasicNameValuePair;
  
  import java.io.UnsupportedEncodingException;
  import java.net.SocketTimeoutException;
  import java.net.URI;
  import java.net.URISyntaxException;
  import java.net.URLDecoder;
  import java.net.URLEncoder;
  import java.nio.charset.Charset;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.TimeoutException;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import static com.alibaba.nacos.common.constant.RequestUrlConstants.HTTPS_PREFIX;
  import static com.alibaba.nacos.common.constant.RequestUrlConstants.HTTP_PREFIX;
  
  /**
   * Http utils.
   *
   * @author <a href="mailto:liaochuntao@live.com">liaochuntao</a>
   */
  public final class HttpUtils {
      
      private static final Pattern CONTEXT_PATH_MATCH = Pattern.compile("(\\/)\\1+");
      
      /**
       * Init http header.
       *
       * @param requestBase requestBase {@link HttpUriRequestBase}
       * @param header      header
       */
      public static void initRequestHeader(ClassicHttpRequest requestBase, Header header) {
          Iterator<Map.Entry<String, String>> iterator = header.iterator();
          while (iterator.hasNext()) {
              Map.Entry<String, String> entry = iterator.next();
              requestBase.setHeader(entry.getKey(), entry.getValue());
          }
      }
      
      /**
       * Init http entity.
       *
       * @param requestBase requestBase {@link HttpUriRequestBase}
       * @param body        body
       * @param header      request header
       * @throws Exception exception
       */
      public static void initRequestEntity(ClassicHttpRequest requestBase, Object body, Header header) throws Exception {
          if (body == null) {
              return;
          }
          if (requestBase instanceof HttpEntityContainer) {
              HttpEntityContainer request = requestBase;
              MediaType mediaType = MediaType.valueOf(header.getValue(HttpHeaderConsts.CONTENT_TYPE));
              ContentType contentType = ContentType.create(mediaType.getType(), mediaType.getCharset());
              HttpEntity entity;
              if (body instanceof byte[]) {
                  entity = new ByteArrayEntity((byte[]) body, contentType);
              } else {
                  entity = new StringEntity(body instanceof String ? (String) body : JacksonUtils.toJson(body),
                          contentType);
              }
              request.setEntity(entity);
          }
      }
      
      /**
       * Init request from entity map.
       *
       * @param requestBase requestBase {@link HttpUriRequestBase}
       * @param body        body map
       * @param charset     charset of entity
       * @throws Exception exception
       */
      public static void initRequestFromEntity(ClassicHttpRequest requestBase, Map<String, String> body, String charset)
              throws Exception {
          if (body == null || body.isEmpty()) {
              return;
          }
          List<NameValuePair> params = new ArrayList<>(body.size());
          for (Map.Entry<String, String> entry : body.entrySet()) {
              params.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
          }
          if (requestBase instanceof HttpEntityContainer) {
              HttpEntityContainer request = requestBase;
              HttpEntity entity = new UrlEncodedFormEntity(params, Charset.forName(charset));
              request.setEntity(entity);
          }
      }
      
      /**
       * Build URL.
       *
       * @param isHttps    whether is https
       * @param serverAddr server ip/address
       * @param subPaths   api path
       * @return URL string
       */
      public static String buildUrl(boolean isHttps, String serverAddr, String... subPaths) {
          StringBuilder sb = new StringBuilder();
          if (isHttps) {
              sb.append(HTTPS_PREFIX);
          } else {
              sb.append(HTTP_PREFIX);
          }
          sb.append(serverAddr);
          String pre = null;
          for (String subPath : subPaths) {
              if (StringUtils.isBlank(subPath)) {
                  continue;
              }
              Matcher matcher = CONTEXT_PATH_MATCH.matcher(subPath);
              if (matcher.find()) {
                  throw new IllegalArgumentException("Illegal url path expression : " + subPath);
              }
              if (pre == null || !pre.endsWith("/")) {
                  if (subPath.startsWith("/")) {
                      sb.append(subPath);
                  } else {
                      sb.append('/').append(subPath);
                  }
              } else {
                  if (subPath.startsWith("/")) {
                      sb.append(subPath.replaceFirst("\\/", ""));
                  } else {
                      sb.append(subPath);
                  }
              }
              pre = subPath;
          }
          return sb.toString();
      }
      
      /**
       * Translate parameter map.
       *
       * @param parameterMap parameter map
       * @return parameter map
       * @throws Exception exception
       */
      public static Map<String, String> translateParameterMap(Map<String, String[]> parameterMap) throws Exception {
          Map<String, String> map = new HashMap<>(16);
          for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
              map.put(entry.getKey(), entry.getValue()[0]);
          }
          return map;
      }
      
      /**
       * Encoding parameters to url string.
       *
       * @param params   parameters
       * @param encoding encoding charset
       * @return url string
       * @throws UnsupportedEncodingException if encoding string is illegal
       */
      public static String encodingParams(Map<String, String> params, String encoding)
              throws UnsupportedEncodingException {
          StringBuilder sb = new StringBuilder();
          if (null == params || params.isEmpty()) {
              return null;
          }
          for (Map.Entry<String, String> entry : params.entrySet()) {
              if (StringUtils.isEmpty(entry.getValue())) {
                  continue;
              }
              
              sb.append(entry.getKey()).append('=');
              sb.append(URLEncoder.encode(entry.getValue(), encoding));
              sb.append('&');
          }
          
          return sb.toString();
      }
      
      /**
       * Encoding KV list to url string.
       *
       * @param paramValues parameters
       * @param encoding    encoding charset
       * @return url string
       * @throws UnsupportedEncodingException if encoding string is illegal
       */
      public static String encodingParams(List<String> paramValues, String encoding) throws UnsupportedEncodingException {
          StringBuilder sb = new StringBuilder();
          if (null == paramValues) {
              return null;
          }
          
          for (Iterator<String> iter = paramValues.iterator(); iter.hasNext(); ) {
              sb.append(iter.next()).append('=');
              sb.append(URLEncoder.encode(iter.next(), encoding));
              if (iter.hasNext()) {
                  sb.append('&');
              }
          }
          return sb.toString();
      }
      
      public static String decode(String str, String encode) throws UnsupportedEncodingException {
          return innerDecode(null, str, encode);
      }
      
      /**
       * build URI By url and query.
       *
       * @param url   url
       * @param query query param {@link Query}
       * @return {@link URI}
       */
      public static URI buildUri(String url, Query query) throws URISyntaxException {
          if (query != null && !query.isEmpty()) {
              url = url + "?" + query.toQueryUrl();
          }
          return new URI(url);
      }
      
      /**
       * HTTP request exception is a timeout exception.
       *
       * @param throwable http request throwable
       * @return boolean
       */
      public static boolean isTimeoutException(Throwable throwable) {
          return throwable instanceof SocketTimeoutException || throwable instanceof ConnectTimeoutException
                  || throwable instanceof TimeoutException || throwable.getCause() instanceof TimeoutException;
      }
      
      /**
       * Build header.
       *
       * @return header
       */
      public static Header builderHeader(String module) {
          Header header = Header.newInstance();
          header.addParam(HttpHeaderConsts.CLIENT_VERSION_HEADER, VersionUtils.version);
          header.addParam(HttpHeaderConsts.USER_AGENT_HEADER, VersionUtils.getFullClientVersion());
          header.addParam(HttpHeaderConsts.ACCEPT_ENCODING, "gzip,deflate,sdch");
          header.addParam(HttpHeaderConsts.CONNECTION, "Keep-Alive");
          header.addParam(HttpHeaderConsts.REQUEST_ID, UuidUtils.generateUuid());
          header.addParam(HttpHeaderConsts.REQUEST_MODULE, module);
          return header;
      }
      
      private static String innerDecode(String pre, String now, String encode) throws UnsupportedEncodingException {
          // Because the data may be encoded by the URL more than once,
          // it needs to be decoded recursively until it is fully successful
          if (StringUtils.equals(pre, now)) {
              return pre;
          }
          pre = now;
          now = URLDecoder.decode(now, encode);
          return innerDecode(pre, now, encode);
      }
      
  }
  