content: package org.araymond.joal.core.client.emulated;
  
  import com.google.common.annotations.VisibleForTesting;
  import com.google.common.base.Preconditions;
  import com.turn.ttorrent.common.protocol.TrackerMessage.AnnounceRequestMessage.RequestEvent;
  import lombok.EqualsAndHashCode;
  import lombok.Getter;
  import org.apache.commons.lang3.StringUtils;
  import org.araymond.joal.core.bandwith.TorrentSeedStats;
  import org.araymond.joal.core.client.emulated.generator.UrlEncoder;
  import org.araymond.joal.core.client.emulated.generator.key.KeyGenerator;
  import org.araymond.joal.core.client.emulated.generator.numwant.NumwantProvider;
  import org.araymond.joal.core.client.emulated.generator.peerid.PeerIdGenerator;
  import org.araymond.joal.core.exception.UnrecognizedClientPlaceholder;
  import org.araymond.joal.core.torrent.torrent.InfoHash;
  import org.araymond.joal.core.ttorrent.client.ConnectionHandler;
  
  import java.net.Inet4Address;
  import java.net.Inet6Address;
  import java.net.InetAddress;
  import java.util.*;
  import java.util.AbstractMap.SimpleImmutableEntry;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;
  import static com.google.common.base.StandardSystemProperty.OS_NAME;
  import static com.google.common.collect.ImmutableSet.toImmutableSet;
  import static java.lang.String.valueOf;
  import static java.util.Optional.ofNullable;
  import static org.apache.commons.lang3.StringUtils.EMPTY;
  import static org.araymond.joal.core.client.emulated.BitTorrentClientConfig.HttpHeader;
  
  /**
   * This is our mocked torrent client that represents our specific configured client (eg 'deluge-2.0.3').
   * <p/>
   * It's responsible for composing/formatting the request data sent to the trackers. It
   * makes no requests itself.
   * <p/>
   * Created by raymo on 26/01/2017.
   */
  @EqualsAndHashCode(exclude = "urlEncoder")
  public class BitTorrentClient {
      private final PeerIdGenerator peerIdGenerator;
      private final KeyGenerator keyGenerator;
      private final UrlEncoder urlEncoder;
      @Getter private final String query;
      @Getter private final Set<Map.Entry<String, String>> headers;
      private final NumwantProvider numwantProvider;
  
      private static final Pattern INFOHASH_PTRN = Pattern.compile("\\{infohash}");
      private static final Pattern UPLOADED_PTRN = Pattern.compile("\\{uploaded}");
      private static final Pattern DOWNLOADED_PTRN = Pattern.compile("\\{downloaded}");
      private static final Pattern LEFT_PTRN = Pattern.compile("\\{left}");
      private static final Pattern PORT_PTRN = Pattern.compile("\\{port}");
      private static final Pattern NUMWANT_PTRN = Pattern.compile("\\{numwant}");
      private static final Pattern PEER_ID_PTRN = Pattern.compile("\\{peerid}");
      private static final Pattern EVENT_PTRN = Pattern.compile("\\{event}");
      private static final Pattern KEY_PTRN = Pattern.compile("\\{key}");
      private static final Pattern JAVA_PTRN = Pattern.compile("\\{java}");
      private static final Pattern OS_PTRN = Pattern.compile("\\{os}");
      private static final Pattern LOCALE_PTRN = Pattern.compile("\\{locale}");
      private static final Pattern AMPERSAND_DUPES_PTRN = Pattern.compile("&{2,}");
      private static final Pattern IP_PTRN = Pattern.compile("\\{ip}");
      private static final Pattern IPV6_PTRN = Pattern.compile("\\{ipv6}");
      private static final Pattern IP_Q_PTRN = Pattern.compile("&*\\w+=\\{ip(?:v6)?}");
      private static final Pattern EVENT_Q_PTRN = Pattern.compile("&*\\w+=\\{event}");
      private static final Pattern PLACEHOLDER_PTRN = Pattern.compile("\\{.*?}");
  
      BitTorrentClient(final PeerIdGenerator peerIdGenerator, final KeyGenerator keyGenerator, final UrlEncoder urlEncoder,
                       final String query, final Collection<HttpHeader> headers, final NumwantProvider numwantProvider) {
          Preconditions.checkNotNull(peerIdGenerator, "peerIdGenerator cannot be null or empty");
          Preconditions.checkNotNull(urlEncoder, "urlEncoder cannot be null");
          Preconditions.checkArgument(!StringUtils.isBlank(query), "query cannot be null or empty");
          Preconditions.checkNotNull(headers, "headers cannot be null");
          Preconditions.checkNotNull(numwantProvider, "numwantProvider cannot be null");
          this.peerIdGenerator = peerIdGenerator;
          this.urlEncoder = urlEncoder;
          this.query = AMPERSAND_DUPES_PTRN.matcher(query).replaceAll("&");  // collapse dupes;
          this.headers = createRequestHeaders(headers);
          this.keyGenerator = keyGenerator;
          this.numwantProvider = numwantProvider;
      }
  
      @VisibleForTesting
      public String getPeerId(final InfoHash infoHash, final RequestEvent event) {
          return peerIdGenerator.getPeerId(infoHash, event);
      }
  
      @VisibleForTesting
      Optional<String> getKey(final InfoHash infoHash, final RequestEvent event) {
          return ofNullable(keyGenerator)
                  .map(keyGen -> keyGen.getKey(infoHash, event));
      }
  
      @VisibleForTesting
      int getNumwant(final RequestEvent event) {
          return this.numwantProvider.get(event);
      }
  
      /**
       * For torrent protocol, see https://wiki.theory.org/BitTorrent_Tracker_Protocol
       */
      public String createRequestQuery(final RequestEvent event, final InfoHash torrentInfoHash,
                                       final TorrentSeedStats stats, final ConnectionHandler connectionHandler) {
          String q = INFOHASH_PTRN.matcher(this.getQuery()).replaceAll(urlEncoder.encode(torrentInfoHash.value()));
          q = UPLOADED_PTRN.matcher(q).replaceAll(valueOf(stats.getUploaded()));
          q = DOWNLOADED_PTRN.matcher(q).replaceAll(valueOf(stats.getDownloaded()));
          q = LEFT_PTRN.matcher(q).replaceAll(valueOf(stats.getLeft()));
          q = PORT_PTRN.matcher(q).replaceAll(valueOf(connectionHandler.getPort()));
          q = NUMWANT_PTRN.matcher(q).replaceAll(valueOf(this.getNumwant(event)));
  
          final String peerId = this.peerIdGenerator.isShouldUrlEncode()
                  ? urlEncoder.encode(this.getPeerId(torrentInfoHash, event))
                  : this.getPeerId(torrentInfoHash, event);
          q = PEER_ID_PTRN.matcher(q).replaceAll(peerId);
  
          // set ip or ipv6 then remove placeholders that were left empty
          InetAddress addy = connectionHandler.getIpAddress();
          if (q.contains("{ip}") && addy instanceof Inet4Address) {
              q = IP_PTRN.matcher(q).replaceAll(addy.getHostAddress());
          } else if (q.contains("{ipv6}") && addy instanceof Inet6Address) {
              q = IPV6_PTRN.matcher(q).replaceAll(urlEncoder.encode(addy.getHostAddress()));
          }
          q = IP_Q_PTRN.matcher(q).replaceAll(EMPTY);
  
          if (event == null || event == RequestEvent.NONE) {
              // if event was NONE, remove the event from the query string; this is the normal announce made at regular intervals.
              q = EVENT_Q_PTRN.matcher(q).replaceAll(EMPTY);
          } else {
              q = EVENT_PTRN.matcher(q).replaceAll(event.getEventName());
          }
  
          if (q.contains("{key}")) {
              final String key = this.getKey(torrentInfoHash, event)
                      .orElseThrow(() -> new IllegalStateException("Client request query contains 'key' but BitTorrentClient does not have a key"));
              q = KEY_PTRN.matcher(q).replaceAll(urlEncoder.encode(key));
          }
  
          final Matcher placeholderMatcher = PLACEHOLDER_PTRN.matcher(q);
          if (placeholderMatcher.find()) {
              throw new UnrecognizedClientPlaceholder("Placeholder [" + placeholderMatcher.group() + "] were not recognized while building announce URL");
          }
  
          if (q.endsWith("&")) {
              q = q.substring(0, q.length() - 1);
          }
          if (q.startsWith("&")) {
              q = q.substring(1);
          }
  
          return q;
      }
  
      private Set<Map.Entry<String, String>> createRequestHeaders(Collection<HttpHeader> headers) {
          return headers.stream().map(hdr -> {
              String value = JAVA_PTRN.matcher(hdr.getValue()).replaceAll(System.getProperty(JAVA_VERSION.key()));
              value = OS_PTRN.matcher(value).replaceAll(System.getProperty(OS_NAME.key()));
              value = LOCALE_PTRN.matcher(value).replaceAll(Locale.getDefault().toLanguageTag());
  
              final Matcher placeholderMatcher = PLACEHOLDER_PTRN.matcher(value);
              if (placeholderMatcher.find()) {
                  throw new UnrecognizedClientPlaceholder("Placeholder [" + placeholderMatcher.group() + "] were not recognized while building client Headers");
              }
  
              return new SimpleImmutableEntry<>(hdr.getName(), value);
          }).collect(toImmutableSet());
      }
  }
  