content: /*
   * Copyright 1999-2020 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
   *
   *      https://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.csp.sentinel.datasource.eureka;
  
  import com.alibaba.csp.sentinel.datasource.AutoRefreshDataSource;
  import com.alibaba.csp.sentinel.datasource.Converter;
  import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
  import com.alibaba.csp.sentinel.log.RecordLog;
  import com.alibaba.csp.sentinel.util.AssertUtil;
  import com.alibaba.csp.sentinel.util.StringUtil;
  import com.alibaba.fastjson.JSON;
  
  import java.io.*;
  import java.net.HttpURLConnection;
  import java.net.InetAddress;
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
  /**
   * <p>
   * A {@link ReadableDataSource} based on Eureka. This class will automatically
   * fetches the metadata of the instance every period.
   * </p>
   * <p>
   * Limitations: Default refresh interval is 10s. Because there is synchronization between eureka servers,
   * it may take longer to take effect.
   * </p>
   *
   * @author liyang
   * @since 1.8.0
   */
  public class EurekaDataSource<T> extends AutoRefreshDataSource<String, T> {
  
      private static final long DEFAULT_REFRESH_MS = 10000;
  
      /**
       * Default connect timeout: 3s
       */
      private static final int DEFAULT_CONNECT_TIMEOUT_MS = 3000;
  
      /**
       * Default read timeout: 30s
       */
      private static final int DEFAULT_READ_TIMEOUT_MS = 30000;
  
      private final int connectTimeoutMills;
      private final int readTimeoutMills;
  
      /**
       * Eureka instance app ID.
       */
      private final String appId;
      /**
       * Eureka instance id.
       */
      private final String instanceId;
  
      /**
       * Eureka server URL list.
       */
      private final List<String> serviceUrls;
  
      /**
       * Metadata key of the rule source.
       */
      private final String ruleKey;
  
      public EurekaDataSource(String appId, String instanceId, List<String> serviceUrls, String ruleKey,
                              Converter<String, T> configParser) {
          this(appId, instanceId, serviceUrls, ruleKey, configParser, DEFAULT_REFRESH_MS, DEFAULT_CONNECT_TIMEOUT_MS,
              DEFAULT_READ_TIMEOUT_MS);
      }
  
      public EurekaDataSource(String appId, String instanceId, List<String> serviceUrls, String ruleKey,
                              Converter<String, T> configParser, long refreshMs, int connectTimeoutMills,
                              int readTimeoutMills) {
          super(configParser, refreshMs);
          AssertUtil.notNull(appId, "appId can't be null");
          AssertUtil.notNull(instanceId, "instanceId can't be null");
          AssertUtil.assertNotEmpty(serviceUrls, "serviceUrls can't be empty");
          AssertUtil.notNull(ruleKey, "ruleKey can't be null");
          AssertUtil.assertState(connectTimeoutMills > 0, "connectTimeoutMills must be greater than 0");
          AssertUtil.assertState(readTimeoutMills > 0, "readTimeoutMills must be greater than 0");
  
          this.appId = appId;
          this.instanceId = instanceId;
          this.serviceUrls = ensureEndWithSlash(serviceUrls);
          AssertUtil.assertNotEmpty(this.serviceUrls, "No available service url");
          this.ruleKey = ruleKey;
          this.connectTimeoutMills = connectTimeoutMills;
          this.readTimeoutMills = readTimeoutMills;
      }
  
      private List<String> ensureEndWithSlash(List<String> serviceUrls) {
          List<String> newServiceUrls = new ArrayList<>();
          for (String serviceUrl : serviceUrls) {
              if (StringUtil.isBlank(serviceUrl)) {
                  continue;
              }
              if (!serviceUrl.endsWith("/")) {
                  serviceUrl = serviceUrl + "/";
              }
              newServiceUrls.add(serviceUrl);
          }
          return newServiceUrls;
      }
  
      @Override
      public String readSource() throws Exception {
          return fetchStringSourceFromEurekaMetadata(this.appId, this.instanceId, this.serviceUrls, ruleKey);
      }
  
      private String fetchStringSourceFromEurekaMetadata(String appId, String instanceId, List<String> serviceUrls,
                                                         String ruleKey) throws Exception {
          List<String> shuffleUrls = new ArrayList<>(serviceUrls.size());
          shuffleUrls.addAll(serviceUrls);
          Collections.shuffle(shuffleUrls);
          for (int i = 0; i < shuffleUrls.size(); i++) {
              String serviceUrl = shuffleUrls.get(i) + String.format("apps/%s/%s", appId, instanceId);
              HttpURLConnection conn = null;
              try {
                  conn = (HttpURLConnection) new URL(serviceUrl).openConnection();
                  conn.addRequestProperty("Accept", "application/json;charset=utf-8");
  
                  conn.setConnectTimeout(connectTimeoutMills);
                  conn.setReadTimeout(readTimeoutMills);
                  conn.setRequestMethod("GET");
                  conn.setDoOutput(true);
                  conn.connect();
                  RecordLog.debug("[EurekaDataSource] Request from eureka server: " + serviceUrl);
                  if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                      String s = toString(conn.getInputStream());
                      String ruleString = JSON.parseObject(s)
                          .getJSONObject("instance")
                          .getJSONObject("metadata")
                          .getString(ruleKey);
                      return ruleString;
                  }
                  RecordLog.warn("[EurekaDataSource] Warn: retrying on another server if available " +
                          "due to response code: {}, response message: {}", conn.getResponseCode(),
                      toString(conn.getErrorStream()));
              } catch (Exception e) {
                  try {
                      if (conn != null) {
                          RecordLog.warn("[EurekaDataSource] Warn: failed to request " + conn.getURL() + " from "
                              + InetAddress.getByName(conn.getURL().getHost()).getHostAddress(), e);
                      }
                  } catch (Exception e1) {
                      RecordLog.warn("[EurekaDataSource] Warn: failed to request ", e1);
                      //ignore
                  }
                  RecordLog.warn("[EurekaDataSource] Warn: failed to request,retrying on another server if available");
  
              } finally {
                  if (conn != null) {
                      conn.disconnect();
                  }
              }
          }
          throw new EurekaMetadataFetchException("Can't get any data");
      }
  
      public static class EurekaMetadataFetchException extends Exception {
  
          public EurekaMetadataFetchException(String message) {
              super(message);
          }
      }
  
      private String toString(InputStream input) throws IOException {
          if (input == null) {
              return null;
          }
          InputStreamReader inputStreamReader = new InputStreamReader(input, "utf-8");
          CharArrayWriter sw = new CharArrayWriter();
          copy(inputStreamReader, sw);
          return sw.toString();
      }
  
      private long copy(Reader input, Writer output) throws IOException {
          char[] buffer = new char[1 << 12];
          long count = 0;
          for (int n = 0; (n = input.read(buffer)) >= 0; ) {
              output.write(buffer, 0, n);
              count += n;
          }
          return count;
      }
  
  }
  