tracker

Change-Id: I8f8ac81f9c4d7c7650cd64d2dade701dc6c11dce
diff --git a/ttorrent-master/common/pom.xml b/ttorrent-master/common/pom.xml
new file mode 100644
index 0000000..909c3b8
--- /dev/null
+++ b/ttorrent-master/common/pom.xml
@@ -0,0 +1,26 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.turn</groupId>
+        <artifactId>ttorrent</artifactId>
+        <version>1.3.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <name>ttorrent/common</name>
+    <url>http://turn.github.com/ttorrent/</url>
+    <artifactId>ttorrent-common</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-bencoding</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/Constants.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/Constants.java
new file mode 100644
index 0000000..001bda2
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/Constants.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2013 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author Sergey.Pak
+ * Date: 9/19/13
+ * Time: 2:57 PM
+ */
+public class Constants {
+  public static final int DEFAULT_ANNOUNCE_INTERVAL_SEC = 15;
+
+  public final static int DEFAULT_SOCKET_CONNECTION_TIMEOUT_MILLIS = 100000;
+  public static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 10000;
+
+  public static final int DEFAULT_MAX_CONNECTION_COUNT = 100;
+
+  public static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
+
+  public static final int DEFAULT_SELECTOR_SELECT_TIMEOUT_MILLIS = 10000;
+  public static final int DEFAULT_CLEANUP_RUN_TIMEOUT_MILLIS = 120000;
+
+  public static final String BYTE_ENCODING = "ISO-8859-1";
+
+  public static final int PIECE_HASH_SIZE = 20;
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/AnnounceableInformation.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/AnnounceableInformation.java
new file mode 100644
index 0000000..234a4d3
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/AnnounceableInformation.java
@@ -0,0 +1,33 @@
+package com.turn.ttorrent.common;
+
+import java.util.List;
+
+public interface AnnounceableInformation extends TorrentHash {
+
+  /**
+   * @return number of bytes uploaded by the client for this torrent
+   */
+  long getUploaded();
+
+  /**
+   * @return number of bytes downloaded by the client for this torrent
+   */
+  long getDownloaded();
+
+  /**
+   * @return number of bytes left to download by the client for this torrent
+   */
+  long getLeft();
+
+  /**
+   * @return all tracker for announce
+   * @see <a href="http://bittorrent.org/beps/bep_0012.html"></a>
+   */
+  List<List<String>> getAnnounceList();
+
+  /**
+   * @return main announce url for tracker
+   */
+  String getAnnounce();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/ImmutableTorrentHash.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/ImmutableTorrentHash.java
new file mode 100644
index 0000000..ee4098c
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/ImmutableTorrentHash.java
@@ -0,0 +1,24 @@
+package com.turn.ttorrent.common;
+
+import java.util.Arrays;
+
+public class ImmutableTorrentHash implements TorrentHash {
+
+  private final byte[] hash;
+  private final String hexHash;
+
+  public ImmutableTorrentHash(byte[] hash) {
+    this.hash = hash;
+    this.hexHash = TorrentUtils.byteArrayToHexString(hash);
+  }
+
+  @Override
+  public byte[] getInfoHash() {
+    return Arrays.copyOf(hash, hash.length);
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return hexHash;
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/LoggerUtils.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/LoggerUtils.java
new file mode 100644
index 0000000..5a2e3f4
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/LoggerUtils.java
@@ -0,0 +1,32 @@
+package com.turn.ttorrent.common;
+
+import org.slf4j.Logger;
+
+public final class LoggerUtils {
+
+  public static void warnAndDebugDetails(Logger logger, String message, Throwable t) {
+    logger.warn(message);
+    logger.debug("", t);
+  }
+
+  public static void warnAndDebugDetails(Logger logger, String message, Object arg, Throwable t) {
+    logger.warn(message, arg);
+    logger.debug("", t);
+  }
+
+  public static void warnWithMessageAndDebugDetails(Logger logger, String message, Object arg, Throwable t) {
+    logger.warn(message + ": " + (t.getMessage() != null ? t.getMessage() : t.getClass().getName()), arg);
+    logger.debug("", t);
+  }
+
+  public static void errorAndDebugDetails(Logger logger, String message, Object arg, Throwable t) {
+    logger.error(message, arg);
+    logger.debug("", t);
+  }
+
+  public static void errorAndDebugDetails(Logger logger, String message, Throwable t) {
+    logger.error(message);
+    logger.debug("", t);
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Optional.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Optional.java
new file mode 100644
index 0000000..5e9b84b
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Optional.java
@@ -0,0 +1,46 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.NoSuchElementException;
+
+public final class Optional<T> {
+
+  private static final Optional<?> EMPTY = new Optional();
+
+  @Nullable
+  private final T value;
+
+  public Optional(@NotNull T value) {
+    this.value = value;
+  }
+
+  private Optional() {
+    this.value = null;
+  }
+
+  @NotNull
+  @SuppressWarnings("unchecked")
+  public static <T> Optional<T> of(@Nullable T value) {
+    return value == null ? (Optional<T>) EMPTY : new Optional<T>(value);
+  }
+
+  @NotNull
+  public T get() throws NoSuchElementException {
+    if (value == null) {
+      throw new NoSuchElementException("No value present");
+    }
+    return value;
+  }
+
+  public boolean isPresent() {
+    return value != null;
+  }
+
+  @NotNull
+  public T orElse(@NotNull T defaultValue) {
+    return value != null ? value : defaultValue;
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Pair.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Pair.java
new file mode 100644
index 0000000..48325f1
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Pair.java
@@ -0,0 +1,46 @@
+package com.turn.ttorrent.common;
+
+public class Pair<A, B> {
+
+  private final A myFirst;
+  private final B mySecond;
+
+  public Pair(A first, B second) {
+    myFirst = first;
+    mySecond = second;
+  }
+
+  public A first() {
+    return myFirst;
+  }
+
+  public B second() {
+    return mySecond;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    Pair<?, ?> pair = (Pair<?, ?>) o;
+
+    if (!myFirst.equals(pair.myFirst)) return false;
+    return mySecond.equals(pair.mySecond);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = myFirst.hashCode();
+    result = 31 * result + mySecond.hashCode();
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return "Pair{" +
+            "myFirst=" + myFirst +
+            ", mySecond=" + mySecond +
+            '}';
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Peer.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Peer.java
new file mode 100644
index 0000000..72f00c0
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/Peer.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (C) 2011-2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common;
+
+import com.turn.ttorrent.Constants;
+import org.slf4j.Logger;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+
+
+/**
+ * A basic BitTorrent peer.
+ *
+ * <p>
+ * This class is meant to be a common base for the tracker and client, which
+ * would presumably subclass it to extend its functionality and fields.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+
+// Peer(对等节点)类,用于表示参与文件共享的单个客户端或服务器节点
+// 基础peer类,被tracked peer继承
+// 实现功能: 获取各个类型的peerid
+public class Peer {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(Peer.class);
+
+  private final InetSocketAddress address;//地址和端口
+  private final String hostId;// 格式化为 "IP:端口" 的字符串
+
+  private ByteBuffer peerId;// 主键,唯一标识
+  private volatile String hexPeerId;
+  private volatile String hexInfoHash;
+
+  /**
+   * Instantiate a new peer.
+   *
+   * @param address The peer's address, with port.
+   */
+  public Peer(InetSocketAddress address) {
+    this(address, null);
+  }
+
+  /**
+   * Instantiate a new peer.
+   *
+   * @param ip   The peer's IP address.
+   * @param port The peer's port.
+   */
+  public Peer(String ip, int port) {
+    this(new InetSocketAddress(ip, port), null);
+  }
+
+  /**
+   * Instantiate a new peer.
+   *
+   * @param ip     The peer's IP address.
+   * @param port   The peer's port.
+   * @param peerId The byte-encoded peer ID.
+   */
+  public Peer(String ip, int port, ByteBuffer peerId) {
+    this(new InetSocketAddress(ip, port), peerId);
+  }
+
+  /**
+   * Instantiate a new peer.
+   *
+   * @param address The peer's address, with port.
+   * @param peerId  The byte-encoded peer ID.
+   */
+  public Peer(InetSocketAddress address, ByteBuffer peerId) {
+    this.address = address;
+    this.hostId = String.format("%s:%d",
+            this.address.getAddress(),
+            this.address.getPort());
+
+    this.setPeerId(peerId);
+  }
+
+  /**
+   * Tells whether this peer has a known peer ID yet or not.
+   */
+  public boolean hasPeerId() {
+    return this.peerId != null;
+  }
+
+  /**
+   * Returns the raw peer ID as a {@link ByteBuffer}.
+   */
+  public ByteBuffer getPeerId() {
+    return this.peerId;
+  }
+
+  public byte[] getPeerIdArray() {
+    return peerId == null ? null : peerId.array();
+  }
+
+  /**
+   * Set a peer ID for this peer (usually during handshake).
+   *
+   * @param peerId The new peer ID for this peer.
+   */
+  public void setPeerId(ByteBuffer peerId) {
+    if (peerId != null) {
+      this.peerId = peerId;
+      this.hexPeerId = TorrentUtils.byteArrayToHexString(peerId.array());
+    } else {
+      this.peerId = null;
+      this.hexPeerId = null;
+    }
+  }
+
+  public String getStringPeerId() {
+    try {
+      return new String(peerId.array(), Constants.BYTE_ENCODING);
+    } catch (UnsupportedEncodingException e) {
+      LoggerUtils.warnAndDebugDetails(logger, "can not get peer id as string", e);
+    }
+    return null;
+  }
+
+  /**
+   * Get the hexadecimal-encoded string representation of this peer's ID.
+   */
+  public String getHexPeerId() {
+    return this.hexPeerId;
+  }
+
+  /**
+   * Get the shortened hexadecimal-encoded peer ID.
+   */
+  public String getShortHexPeerId() {
+    return String.format("..%s",
+            this.hexPeerId.substring(this.hexPeerId.length() - 6).toUpperCase());
+  }
+
+  /**
+   * Returns this peer's IP address.
+   */
+  public String getIp() {
+    return this.address.getAddress().getHostAddress();
+  }
+
+  /**
+   * Returns this peer's InetAddress.
+   */
+  public InetSocketAddress getAddress() {
+    return this.address;
+  }
+
+  /**
+   * Returns this peer's port number.
+   */
+  public int getPort() {
+    return this.address.getPort();
+  }
+
+  /**
+   * Returns this peer's host identifier ("host:port").
+   */
+  public String getHostIdentifier() {
+    return this.hostId;
+  }
+
+  /**
+   * Returns a binary representation of the peer's IP.
+   */
+  public byte[] getRawIp() {
+    final InetAddress address = this.address.getAddress();
+    if (address == null) return null;
+    return address.getAddress();
+  }
+
+
+  /**
+   * Tells if two peers seem to look alike (i.e. they have the same IP, port
+   * and peer ID if they have one).
+   */
+  public boolean looksLike(Peer other) {
+    if (other == null) {
+      return false;
+    }
+
+    return this.hostId.equals(other.hostId) && this.getPort() == other.getPort();
+  }
+
+  public void setTorrentHash(String hexInfoHash) {
+    this.hexInfoHash = hexInfoHash;
+  }
+
+  public String getHexInfoHash() {
+    return hexInfoHash;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    Peer peer = (Peer) o;
+
+    if (hexPeerId == null && peer.hexPeerId == null) return super.equals(o);
+
+    if (hexPeerId != null ? !hexPeerId.equals(peer.hexPeerId) : peer.hexPeerId != null) return false;
+    return hexInfoHash != null ? hexInfoHash.equals(peer.hexInfoHash) : peer.hexInfoHash == null;
+  }
+
+  @Override
+  public int hashCode() {
+
+    if (hexPeerId == null) return super.hashCode();
+
+    int result = hexPeerId != null ? hexPeerId.hashCode() : 0;
+    result = 31 * result + (hexInfoHash != null ? hexInfoHash.hashCode() : 0);
+    return result;
+  }
+
+  /**
+   * Returns a human-readable representation of this peer.
+   */
+  @Override
+  public String toString() {
+    return "Peer " + address + " for torrent " + hexInfoHash;
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/PeerUID.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/PeerUID.java
new file mode 100644
index 0000000..bac1b02
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/PeerUID.java
@@ -0,0 +1,51 @@
+package com.turn.ttorrent.common;
+
+import java.net.InetSocketAddress;
+
+/* 保存用户的地址相关信息
+* 标识唯一一个用户和用户相关的torrent地址
+* */
+public class PeerUID {
+
+  private final InetSocketAddress myAddress;// 保存ip地址,端口号,主机名
+  private final String myTorrentHash;// 标识唯一的torrent文件
+
+  public PeerUID(InetSocketAddress address, String torrentHash) {
+    myAddress = address;
+    myTorrentHash = torrentHash;
+  }
+
+  public String getTorrentHash() {
+    return myTorrentHash;
+  }
+
+  public InetSocketAddress getAddress() {
+    return myAddress;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    PeerUID peerUID = (PeerUID) o;
+
+    if (!myAddress.equals(peerUID.myAddress)) return false;
+    return myTorrentHash.equals(peerUID.myTorrentHash);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = myAddress.hashCode();
+    result = 31 * result + myTorrentHash.hashCode();
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return "PeerUID{" +
+            "address=" + myAddress +
+            ", torrent hash='" + myTorrentHash + '\'' +
+            '}';
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/SystemTimeService.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/SystemTimeService.java
new file mode 100644
index 0000000..6a55cd1
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/SystemTimeService.java
@@ -0,0 +1,10 @@
+package com.turn.ttorrent.common;
+
+public class SystemTimeService implements TimeService {
+
+  @Override
+  public long now() {
+    return System.currentTimeMillis();
+  }
+  // 返回自 Unix 纪元(1970-01-01 00:00:00 UTC) 以来的毫秒数(long 类型)
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TimeService.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TimeService.java
new file mode 100644
index 0000000..ac41ffd
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TimeService.java
@@ -0,0 +1,14 @@
+package com.turn.ttorrent.common;
+
+/**
+ * Abstract time service. Provides current time millis.
+ */
+public interface TimeService {
+  /**
+   * Provides current time millis.
+   *
+   * @return current time.
+   */
+  long now();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentCreator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentCreator.java
new file mode 100644
index 0000000..c5de1ef
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentCreator.java
@@ -0,0 +1,376 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.common.creation.MetadataBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.io.*;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+/**
+ * Old API for creating .torrent files, use {@link MetadataBuilder}
+ * @deprecated
+ */
+@Deprecated
+public class TorrentCreator {
+
+  private final static Logger logger = TorrentLoggerFactory.getLogger(TorrentCreator.class);
+
+  /**
+   * Torrent file piece length (in bytes), we use 512 kB.
+   */
+  public static final int DEFAULT_PIECE_LENGTH = 512 * 1024;
+  private static final int HASHING_TIMEOUT_SEC = 15;
+  public static int HASHING_THREADS_COUNT = Runtime.getRuntime().availableProcessors();
+
+  static {
+    String threads = System.getenv("TTORRENT_HASHING_THREADS");
+
+    if (threads != null) {
+      try {
+        int count = Integer.parseInt(threads);
+        if (count > 0) {
+          TorrentCreator.HASHING_THREADS_COUNT = count;
+        }
+      } catch (NumberFormatException nfe) {
+        // Pass
+      }
+    }
+  }
+
+  private static final ExecutorService HASHING_EXECUTOR = Executors.newFixedThreadPool(HASHING_THREADS_COUNT, new ThreadFactory() {
+    @Override
+    public Thread newThread(@NotNull final Runnable r) {
+      final Thread thread = new Thread(r);
+      thread.setDaemon(true);
+      return thread;
+    }
+  });
+
+  /**
+   * Create a {@link TorrentMetadata} object for a file.
+   *
+   * <p>
+   * Hash the given file to create the {@link TorrentMetadata} object representing
+   * the Torrent meta info about this file, needed for announcing and/or
+   * sharing said file.
+   * </p>
+   *
+   * @param source    The file to use in the torrent.
+   * @param announce  The announce URI that will be used for this torrent.
+   * @param createdBy The creator's name, or any string identifying the
+   *                  torrent's creator.
+   */
+  public static TorrentMetadata create(File source, URI announce, String createdBy)
+          throws InterruptedException, IOException {
+    return create(source, null, announce, createdBy);
+  }
+
+  /**
+   * Create a {@link TorrentMetadata} object for a set of files.
+   *
+   * <p>
+   * Hash the given files to create the multi-file {@link TorrentMetadata} object
+   * representing the Torrent meta-info about them, needed for announcing
+   * and/or sharing these files. Since we created the torrent, we're
+   * considering we'll be a full initial seeder for it.
+   * </p>
+   *
+   * @param parent    The parent directory or location of the torrent files,
+   *                  also used as the torrent's name.
+   * @param files     The files to add into this torrent.
+   * @param announce  The announce URI that will be used for this torrent.
+   * @param createdBy The creator's name, or any string identifying the
+   *                  torrent's creator.
+   */
+  public static TorrentMetadata create(File parent, List<File> files, URI announce,
+                                       String createdBy) throws InterruptedException, IOException {
+    return create(parent, files, announce, null, createdBy);
+  }
+
+  /**
+   * Create a {@link TorrentMetadata} object for a file.
+   *
+   * <p>
+   * Hash the given file to create the {@link TorrentMetadata} object representing
+   * the Torrent metainfo about this file, needed for announcing and/or
+   * sharing said file.
+   * </p>
+   *
+   * @param source       The file to use in the torrent.
+   * @param announceList The announce URIs organized as tiers that will
+   *                     be used for this torrent
+   * @param createdBy    The creator's name, or any string identifying the
+   *                     torrent's creator.
+   */
+  public static TorrentMetadata create(File source, List<List<URI>> announceList,
+                                       String createdBy) throws InterruptedException, IOException {
+    return create(source, null, null, announceList, createdBy);
+  }
+
+  /**
+   * Create a {@link TorrentMetadata} object for a set of files.
+   *
+   * <p>
+   * Hash the given files to create the multi-file {@link TorrentMetadata} object
+   * representing the Torrent meta-info about them, needed for announcing
+   * and/or sharing these files. Since we created the torrent, we're
+   * considering we'll be a full initial seeder for it.
+   * </p>
+   *
+   * @param source       The parent directory or location of the torrent files,
+   *                     also used as the torrent's name.
+   * @param files        The files to add into this torrent.
+   * @param announceList The announce URIs organized as tiers that will
+   *                     be used for this torrent
+   * @param createdBy    The creator's name, or any string identifying the
+   *                     torrent's creator.
+   */
+  public static TorrentMetadata create(File source, List<File> files,
+                                       List<List<URI>> announceList, String createdBy)
+          throws InterruptedException, IOException {
+    return create(source, files, null, announceList, createdBy);
+  }
+
+  /**
+   * Helper method to create a {@link TorrentMetadata} object for a set of files.
+   *
+   * <p>
+   * Hash the given files to create the multi-file {@link TorrentMetadata} object
+   * representing the Torrent meta-info about them, needed for announcing
+   * and/or sharing these files. Since we created the torrent, we're
+   * considering we'll be a full initial seeder for it.
+   * </p>
+   *
+   * @param parent       The parent directory or location of the torrent files,
+   *                     also used as the torrent's name.
+   * @param files        The files to add into this torrent.
+   * @param announce     The announce URI that will be used for this torrent.
+   * @param announceList The announce URIs organized as tiers that will
+   *                     be used for this torrent
+   * @param createdBy    The creator's name, or any string identifying the
+   *                     torrent's creator.
+   */
+  public static TorrentMetadata create(File parent, List<File> files, URI announce, List<List<URI>> announceList, String createdBy)
+          throws InterruptedException, IOException {
+    return create(parent, files, announce, announceList, createdBy, DEFAULT_PIECE_LENGTH);
+  }
+
+  public static TorrentMetadata create(File parent, List<File> files, URI announce,
+                                       List<List<URI>> announceList, String createdBy, final int pieceSize)
+          throws InterruptedException, IOException {
+    return create(parent, files, announce, announceList, createdBy, System.currentTimeMillis() / 1000, pieceSize);
+  }
+
+  //for tests
+  /*package local*/
+  static TorrentMetadata create(File parent, List<File> files, URI announce,
+                                List<List<URI>> announceList, String createdBy, long creationTimeSecs, final int pieceSize)
+          throws InterruptedException, IOException {
+    Map<String, BEValue> torrent = new HashMap<String, BEValue>();
+
+    if (announce != null) {
+      torrent.put(ANNOUNCE, new BEValue(announce.toString()));
+    }
+    if (announceList != null) {
+      List<BEValue> tiers = new LinkedList<BEValue>();
+      for (List<URI> trackers : announceList) {
+        List<BEValue> tierInfo = new LinkedList<BEValue>();
+        for (URI trackerURI : trackers) {
+          tierInfo.add(new BEValue(trackerURI.toString()));
+        }
+        tiers.add(new BEValue(tierInfo));
+      }
+      torrent.put(ANNOUNCE_LIST, new BEValue(tiers));
+    }
+    torrent.put(CREATION_DATE_SEC, new BEValue(creationTimeSecs));
+    torrent.put(CREATED_BY, new BEValue(createdBy));
+
+    Map<String, BEValue> info = new TreeMap<String, BEValue>();
+    info.put(NAME, new BEValue(parent.getName()));
+    info.put(PIECE_LENGTH, new BEValue(pieceSize));
+
+    if (files == null || files.isEmpty()) {
+      info.put(FILE_LENGTH, new BEValue(parent.length()));
+      info.put(PIECES, new BEValue(hashFile(parent, pieceSize),
+              Constants.BYTE_ENCODING));
+    } else {
+      List<BEValue> fileInfo = new LinkedList<BEValue>();
+      for (File file : files) {
+        Map<String, BEValue> fileMap = new HashMap<String, BEValue>();
+        fileMap.put(FILE_LENGTH, new BEValue(file.length()));
+
+        LinkedList<BEValue> filePath = new LinkedList<BEValue>();
+        while (file != null) {
+          if (file.equals(parent)) {
+            break;
+          }
+
+          filePath.addFirst(new BEValue(file.getName()));
+          file = file.getParentFile();
+        }
+
+        fileMap.put(FILE_PATH, new BEValue(filePath));
+        fileInfo.add(new BEValue(fileMap));
+      }
+      info.put(FILES, new BEValue(fileInfo));
+      info.put(PIECES, new BEValue(hashFiles(files, pieceSize),
+              Constants.BYTE_ENCODING));
+    }
+    torrent.put(INFO_TABLE, new BEValue(info));
+
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    BEncoder.bencode(new BEValue(torrent), baos);
+    return new TorrentParser().parse(baos.toByteArray());
+  }
+
+  /**
+   * Return the concatenation of the SHA-1 hashes of a file's pieces.
+   *
+   * <p>
+   * Hashes the given file piece by piece using the default Torrent piece
+   * length (see {@link #DEFAULT_PIECE_LENGTH}) and returns the concatenation of
+   * these hashes, as a string.
+   * </p>
+   *
+   * <p>
+   * This is used for creating Torrent meta-info structures from a file.
+   * </p>
+   *
+   * @param file The file to hash.
+   */
+  private static String hashFile(final File file, final int pieceSize)
+          throws InterruptedException, IOException {
+    return hashFiles(Collections.singletonList(file), pieceSize);
+  }
+
+  private static String hashFiles(final List<File> files, final int pieceSize)
+          throws InterruptedException, IOException {
+    if (files.size() == 0) {
+      return "";
+    }
+    List<Future<String>> results = new LinkedList<Future<String>>();
+    long length = 0L;
+
+    final ByteBuffer buffer = ByteBuffer.allocate(pieceSize);
+
+
+    final AtomicInteger threadIdx = new AtomicInteger(0);
+    final String firstFileName = files.get(0).getName();
+
+    StringBuilder hashes = new StringBuilder();
+
+    long start = System.nanoTime();
+    for (File file : files) {
+      logger.debug("Analyzing local data for {} with {} threads...",
+              file.getName(), HASHING_THREADS_COUNT);
+
+      length += file.length();
+
+      FileInputStream fis = new FileInputStream(file);
+      FileChannel channel = fis.getChannel();
+
+      try {
+        while (channel.read(buffer) > 0) {
+          if (buffer.remaining() == 0) {
+            buffer.clear();
+            final ByteBuffer data = prepareDataFromBuffer(buffer);
+
+            results.add(HASHING_EXECUTOR.submit(new Callable<String>() {
+              @Override
+              public String call() throws Exception {
+                Thread.currentThread().setName(String.format("%s hasher #%d", firstFileName, threadIdx.incrementAndGet()));
+                return new CallableChunkHasher(data).call();
+              }
+            }));
+          }
+
+          if (results.size() >= HASHING_THREADS_COUNT) {
+            // process hashers, otherwise they will spend too much memory
+            waitForHashesToCalculate(results, hashes);
+            results.clear();
+          }
+        }
+      } finally {
+        channel.close();
+        fis.close();
+      }
+    }
+
+    // Hash the last bit, if any
+    if (buffer.position() > 0) {
+      buffer.limit(buffer.position());
+      buffer.position(0);
+      final ByteBuffer data = prepareDataFromBuffer(buffer);
+      results.add(HASHING_EXECUTOR.submit(new CallableChunkHasher(data)));
+    }
+    // here we have only a few hashes to wait for calculation
+    waitForHashesToCalculate(results, hashes);
+
+    long elapsed = System.nanoTime() - start;
+
+    int expectedPieces = (int) (Math.ceil(
+            (double) length / pieceSize));
+    logger.debug("Hashed {} file(s) ({} bytes) in {} pieces ({} expected) in {}ms.",
+            new Object[]{
+                    files.size(),
+                    length,
+                    results.size(),
+                    expectedPieces,
+                    String.format("%.1f", elapsed / 1e6),
+            });
+
+    return hashes.toString();
+  }
+
+  private static ByteBuffer prepareDataFromBuffer(ByteBuffer buffer) {
+    final ByteBuffer data = ByteBuffer.allocate(buffer.remaining());
+    buffer.mark();
+    data.put(buffer);
+    data.clear();
+    buffer.reset();
+    return data;
+  }
+
+  private static void waitForHashesToCalculate(List<Future<String>> results, StringBuilder hashes) throws InterruptedException, IOException {
+    try {
+      for (Future<String> chunk : results) {
+        hashes.append(chunk.get(HASHING_TIMEOUT_SEC, TimeUnit.SECONDS));
+      }
+    } catch (ExecutionException ee) {
+      throw new IOException("Error while hashing the torrent data!", ee);
+    } catch (TimeoutException e) {
+      throw new RuntimeException(String.format("very slow hashing: took more than %d seconds to calculate several pieces. Cancelling", HASHING_TIMEOUT_SEC));
+    }
+  }
+
+  /**
+   * A {@link Callable} to hash a data chunk.
+   *
+   * @author mpetazzoni
+   */
+  private static class CallableChunkHasher implements Callable<String> {
+
+    private final ByteBuffer data;
+
+    CallableChunkHasher(final ByteBuffer data) {
+      this.data = data;
+    }
+
+    @Override
+    public String call() throws UnsupportedEncodingException {
+      byte[] sha1Hash = TorrentUtils.calculateSha1Hash(this.data.array());
+      return new String(sha1Hash, Constants.BYTE_ENCODING);
+    }
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentFile.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentFile.java
new file mode 100644
index 0000000..2443b54
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentFile.java
@@ -0,0 +1,42 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * @author dgiffin
+ * @author mpetazzoni
+ */
+public class TorrentFile {
+
+  @NotNull
+  public final List<String> relativePath;
+  public final long size;
+  @NotNull
+  public final Optional<String> md5Hash;
+
+  public TorrentFile(@NotNull List<String> relativePath, long size, @Nullable String md5Hash) {
+    this.relativePath = new ArrayList<String>(relativePath);
+    this.size = size;
+    this.md5Hash = Optional.of(md5Hash);
+  }
+
+  public String getRelativePathAsString() {
+    String delimiter = File.separator;
+    final Iterator<String> iterator = relativePath.iterator();
+    StringBuilder sb = new StringBuilder();
+    if (iterator.hasNext()) {
+      sb.append(iterator.next());
+      while (iterator.hasNext()) {
+        sb.append(delimiter).append(iterator.next());
+      }
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentHash.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentHash.java
new file mode 100644
index 0000000..7fae345
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentHash.java
@@ -0,0 +1,13 @@
+package com.turn.ttorrent.common;
+
+public interface TorrentHash {
+  /**
+   * Return the hash of the B-encoded meta-info structure of a torrent.
+   */
+  byte[] getInfoHash();
+
+  /**
+   * Get torrent's info hash (as an hexadecimal-coded string).
+   */
+  String getHexInfoHash();
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentInfo.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentInfo.java
new file mode 100644
index 0000000..8bc5fc1
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentInfo.java
@@ -0,0 +1,28 @@
+package com.turn.ttorrent.common;
+
+/**
+ * @author Sergey.Pak
+ * Date: 8/9/13
+ * Time: 6:00 PM
+ */
+public interface TorrentInfo extends TorrentHash {
+
+  /*
+  * Number of bytes uploaded by the client for this torrent
+  * */
+  long getUploaded();
+
+  /*
+  * Number of bytes downloaded by the client for this torrent
+  * */
+  long getDownloaded();
+
+  /*
+  * Number of bytes left to download by the client for this torrent
+  * */
+  long getLeft();
+
+  int getPieceCount();
+
+  long getPieceSize(int pieceIdx);
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentLoggerFactory.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentLoggerFactory.java
new file mode 100644
index 0000000..1b5a545
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentLoggerFactory.java
@@ -0,0 +1,24 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class TorrentLoggerFactory {
+
+  @Nullable
+  private static volatile String staticLoggersName = null;
+
+  public static Logger getLogger(Class<?> clazz) {
+    String name = staticLoggersName;
+    if (name == null) {
+      name = clazz.getName();
+    }
+    return LoggerFactory.getLogger(name);
+  }
+
+  public static void setStaticLoggersName(@Nullable String staticLoggersName) {
+    TorrentLoggerFactory.staticLoggersName = staticLoggersName;
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadata.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadata.java
new file mode 100644
index 0000000..22d6d1e
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadata.java
@@ -0,0 +1,76 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Provided access to all stored info in .torrent file
+ *
+ * @see <a href="https://wiki.theory.org/index.php/BitTorrentSpecification#Metainfo_File_Structure"></a>
+ */
+public interface TorrentMetadata extends TorrentHash {
+
+  /**
+   * @return all tracker for announce
+   * @see <a href="http://bittorrent.org/beps/bep_0012.html"></a>
+   */
+  @Nullable
+  List<List<String>> getAnnounceList();
+
+  /**
+   * @return main announce url for tracker or <code>null</code> if main announce is not specified
+   */
+  @Nullable
+  String getAnnounce();
+
+  /**
+   * @return creation date of the torrent in unix format
+   */
+  Optional<Long> getCreationDate();
+
+  /**
+   * @return free-form text comment of the author
+   */
+  Optional<String> getComment();
+
+  /**
+   * @return name and version of the program used to create .torrent
+   */
+  Optional<String> getCreatedBy();
+
+  /**
+   * @return number of bytes in each piece
+   */
+  int getPieceLength();
+
+  /**
+   * @return concatenation of all 20-byte SHA1 hash values, one per piece.
+   * So the length of this array must be a multiple of 20
+   */
+  byte[] getPiecesHashes();
+
+  /**
+   * @return true if it's private torrent. In this case client must get peers only from tracker and
+   * must initiate connections to peers returned from the tracker.
+   * @see <a href="http://bittorrent.org/beps/bep_0027.html"></a>
+   */
+  boolean isPrivate();
+
+  /**
+   * @return count of pieces in torrent
+   */
+  int getPiecesCount();
+
+  /**
+   * @return The filename of the directory in which to store all the files
+   */
+  String getDirectoryName();
+
+  /**
+   * @return list of files, stored in this torrent
+   */
+  List<TorrentFile> getFiles();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataImpl.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataImpl.java
new file mode 100644
index 0000000..8b831d0
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataImpl.java
@@ -0,0 +1,115 @@
+package com.turn.ttorrent.common;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class TorrentMetadataImpl implements TorrentMetadata {
+
+  private final byte[] myInfoHash;
+  @Nullable
+  private final List<List<String>> myAnnounceList;
+  private final String myMainAnnounce;
+  private final long myCreationDate;
+  private final String myComment;
+  private final String myCreatedBy;
+  private final String myName;
+  private final List<TorrentFile> myFiles;
+  private final int myPieceCount;
+  private final int myPieceLength;
+  private final byte[] myPiecesHashes;
+  private final String myHexString;
+
+  TorrentMetadataImpl(byte[] infoHash,
+                      @Nullable List<List<String>> announceList,
+                      String mainAnnounce,
+                      long creationDate,
+                      String comment,
+                      String createdBy,
+                      String name,
+                      List<TorrentFile> files,
+                      int pieceCount,
+                      int pieceLength,
+                      byte[] piecesHashes) {
+    myInfoHash = infoHash;
+    myAnnounceList = announceList;
+    myMainAnnounce = mainAnnounce;
+    myCreationDate = creationDate;
+    myComment = comment;
+    myCreatedBy = createdBy;
+    myName = name;
+    myFiles = files;
+    myPieceCount = pieceCount;
+    myPieceLength = pieceLength;
+    myPiecesHashes = piecesHashes;
+    myHexString = TorrentUtils.byteArrayToHexString(myInfoHash);
+  }
+
+  @Override
+  public String getDirectoryName() {
+    return myName;
+  }
+
+  @Override
+  public List<TorrentFile> getFiles() {
+    return myFiles;
+  }
+
+  @Nullable
+  @Override
+  public List<List<String>> getAnnounceList() {
+    return myAnnounceList;
+  }
+
+  @Nullable
+  @Override
+  public String getAnnounce() {
+    return myMainAnnounce;
+  }
+
+  @Override
+  public Optional<Long> getCreationDate() {
+    return Optional.of(myCreationDate == -1 ? null : myCreationDate);
+  }
+
+  @Override
+  public Optional<String> getComment() {
+    return Optional.of(myComment);
+  }
+
+  @Override
+  public Optional<String> getCreatedBy() {
+    return Optional.of(myCreatedBy);
+  }
+
+  @Override
+  public int getPieceLength() {
+    return myPieceLength;
+  }
+
+  @Override
+  public byte[] getPiecesHashes() {
+    return myPiecesHashes;
+  }
+
+  @Override
+  public boolean isPrivate() {
+    return false;
+  }
+
+  @Override
+  public int getPiecesCount() {
+    return myPieceCount;
+  }
+
+  @Override
+  public byte[] getInfoHash() {
+    return myInfoHash;
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return myHexString;
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataKeys.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataKeys.java
new file mode 100644
index 0000000..a74a33f
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentMetadataKeys.java
@@ -0,0 +1,23 @@
+package com.turn.ttorrent.common;
+
+@SuppressWarnings("WeakerAccess")
+public final class TorrentMetadataKeys {
+
+  public final static String MD5_SUM = "md5sum";
+  public final static String FILE_LENGTH = "length";
+  public final static String FILES = "files";
+  public final static String FILE_PATH = "path";
+  public final static String FILE_PATH_UTF8 = "path.utf-8";
+  public final static String COMMENT = "comment";
+  public final static String CREATED_BY = "created by";
+  public final static String ANNOUNCE = "announce";
+  public final static String PIECE_LENGTH = "piece length";
+  public final static String PIECES = "pieces";
+  public final static String CREATION_DATE_SEC = "creation date";
+  public final static String PRIVATE = "private";
+  public final static String NAME = "name";
+  public final static String INFO_TABLE = "info";
+  public final static String ANNOUNCE_LIST = "announce-list";
+  public final static String URL_LIST = "url-list";
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentParser.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentParser.java
new file mode 100644
index 0000000..81ea519
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentParser.java
@@ -0,0 +1,162 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import org.apache.commons.io.FileUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+public class TorrentParser {
+
+  public TorrentMetadata parseFromFile(File torrentFile) throws IOException {
+    byte[] fileContent = FileUtils.readFileToByteArray(torrentFile);
+    return parse(fileContent);
+  }
+
+  /**
+   * @param metadata binary .torrent content
+   * @return parsed metadata object. This parser also wraps single torrent as multi torrent with one file
+   * @throws InvalidBEncodingException if metadata has incorrect BEP format or missing required fields
+   * @throws RuntimeException          It's wrapped io exception from bep decoder.
+   *                                   This exception doesn't must to throw io exception because reading from
+   *                                   byte array input stream cannot throw the exception
+   */
+  public TorrentMetadata parse(byte[] metadata) throws InvalidBEncodingException, RuntimeException {
+    final Map<String, BEValue> dictionaryMetadata;
+    try {
+      dictionaryMetadata = BDecoder.bdecode(new ByteArrayInputStream(metadata)).getMap();
+    } catch (InvalidBEncodingException e) {
+      throw e;
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    final Map<String, BEValue> infoTable = getRequiredValueOrThrowException(dictionaryMetadata, INFO_TABLE).getMap();
+
+    final BEValue creationDateValue = dictionaryMetadata.get(CREATION_DATE_SEC);
+    final long creationDate = creationDateValue == null ? -1 : creationDateValue.getLong();
+
+    final String comment = getStringOrNull(dictionaryMetadata, COMMENT);
+    final String createdBy = getStringOrNull(dictionaryMetadata, CREATED_BY);
+    final String announceUrl = getStringOrNull(dictionaryMetadata, ANNOUNCE);
+    final List<List<String>> trackers = getTrackers(dictionaryMetadata);
+    final int pieceLength = getRequiredValueOrThrowException(infoTable, PIECE_LENGTH).getInt();
+    final byte[] piecesHashes = getRequiredValueOrThrowException(infoTable, PIECES).getBytes();
+
+    final boolean torrentContainsManyFiles = infoTable.get(FILES) != null;
+
+    final String dirName = getRequiredValueOrThrowException(infoTable, NAME).getString();
+
+    final List<TorrentFile> files = parseFiles(infoTable, torrentContainsManyFiles, dirName);
+
+    if (piecesHashes.length % Constants.PIECE_HASH_SIZE != 0)
+      throw new InvalidBEncodingException("Incorrect size of pieces hashes");
+
+    final int piecesCount = piecesHashes.length / Constants.PIECE_HASH_SIZE;
+
+    byte[] infoTableBytes;
+    try {
+      infoTableBytes = BEncoder.bencode(infoTable).array();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    return new TorrentMetadataImpl(
+            TorrentUtils.calculateSha1Hash(infoTableBytes),
+            trackers,
+            announceUrl,
+            creationDate,
+            comment,
+            createdBy,
+            dirName,
+            files,
+            piecesCount,
+            pieceLength,
+            piecesHashes
+    );
+  }
+
+  private List<TorrentFile> parseFiles(Map<String, BEValue> infoTable, boolean torrentContainsManyFiles, String name) throws InvalidBEncodingException {
+    if (!torrentContainsManyFiles) {
+      final BEValue md5Sum = infoTable.get(MD5_SUM);
+      return Collections.singletonList(new TorrentFile(
+              Collections.singletonList(name),
+              getRequiredValueOrThrowException(infoTable, FILE_LENGTH).getLong(),
+              md5Sum == null ? null : md5Sum.getString()
+      ));
+    }
+
+    List<TorrentFile> result = new ArrayList<TorrentFile>();
+    for (BEValue file : infoTable.get(FILES).getList()) {
+      Map<String, BEValue> fileInfo = file.getMap();
+      List<String> path = new ArrayList<String>();
+      BEValue filePathList = fileInfo.get(FILE_PATH_UTF8);
+      if (filePathList == null) {
+        filePathList = fileInfo.get(FILE_PATH);
+      }
+      for (BEValue pathElement : filePathList.getList()) {
+        path.add(pathElement.getString());
+      }
+      final BEValue md5Sum = infoTable.get(MD5_SUM);
+      result.add(new TorrentFile(
+              path,
+              fileInfo.get(FILE_LENGTH).getLong(),
+              md5Sum == null ? null : md5Sum.getString()));
+    }
+    return result;
+  }
+
+  @Nullable
+  private String getStringOrNull(Map<String, BEValue> dictionaryMetadata, String key) throws InvalidBEncodingException {
+    final BEValue value = dictionaryMetadata.get(key);
+    if (value == null) return null;
+    return value.getString();
+  }
+
+  @Nullable
+  private List<List<String>> getTrackers(Map<String, BEValue> dictionaryMetadata) throws InvalidBEncodingException {
+    final BEValue announceListValue = dictionaryMetadata.get(ANNOUNCE_LIST);
+    if (announceListValue == null) return null;
+    List<BEValue> announceList = announceListValue.getList();
+    List<List<String>> result = new ArrayList<List<String>>();
+    Set<String> allTrackers = new HashSet<String>();
+    for (BEValue tv : announceList) {
+      List<BEValue> trackers = tv.getList();
+      if (trackers.isEmpty()) {
+        continue;
+      }
+
+      List<String> tier = new ArrayList<String>();
+      for (BEValue tracker : trackers) {
+        final String url = tracker.getString();
+        if (!allTrackers.contains(url)) {
+          tier.add(url);
+          allTrackers.add(url);
+        }
+      }
+
+      if (!tier.isEmpty()) {
+        result.add(tier);
+      }
+    }
+    return result;
+  }
+
+  @NotNull
+  private BEValue getRequiredValueOrThrowException(Map<String, BEValue> map, String key) throws InvalidBEncodingException {
+    final BEValue value = map.get(key);
+    if (value == null)
+      throw new InvalidBEncodingException("Invalid metadata format. Map doesn't contain required field " + key);
+    return value;
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentSerializer.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentSerializer.java
new file mode 100644
index 0000000..1aa287a
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentSerializer.java
@@ -0,0 +1,93 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+public class TorrentSerializer {
+
+  public byte[] serialize(TorrentMetadata metadata) throws IOException {
+    Map<String, BEValue> mapMetadata = new HashMap<String, BEValue>();
+    Map<String, BEValue> infoTable = new HashMap<String, BEValue>();
+
+    String announce = metadata.getAnnounce();
+    if (announce != null) mapMetadata.put(ANNOUNCE, new BEValue(announce));
+
+    putOptionalIfPresent(mapMetadata, COMMENT, metadata.getComment());
+    putOptionalIfPresent(mapMetadata, CREATED_BY, metadata.getCreatedBy());
+
+    if (metadata.getCreationDate().isPresent())
+      mapMetadata.put(CREATION_DATE_SEC, new BEValue(metadata.getCreationDate().get()));
+
+    List<BEValue> announceList = getAnnounceListAsBEValues(metadata.getAnnounceList());
+    if (announceList != null) {
+      mapMetadata.put(ANNOUNCE_LIST, new BEValue(announceList));
+    }
+    infoTable.put(PIECE_LENGTH, new BEValue(metadata.getPieceLength()));
+    infoTable.put(PIECES, new BEValue(metadata.getPiecesHashes()));
+    if (metadata.isPrivate()) {
+      infoTable.put(PRIVATE, new BEValue(1));
+    }
+
+    infoTable.put(NAME, new BEValue(metadata.getDirectoryName()));
+    if (metadata.getFiles().size() == 1) {
+      final TorrentFile torrentFile = metadata.getFiles().get(0);
+      infoTable.put(FILE_LENGTH, new BEValue(torrentFile.size));
+      putOptionalIfPresent(infoTable, MD5_SUM, torrentFile.md5Hash);
+    } else {
+      List<BEValue> files = new ArrayList<BEValue>();
+      for (TorrentFile torrentFile : metadata.getFiles()) {
+        Map<String, BEValue> entry = new HashMap<String, BEValue>();
+        entry.put(FILE_LENGTH, new BEValue(torrentFile.size));
+        putOptionalIfPresent(entry, MD5_SUM, torrentFile.md5Hash);
+        entry.put(FILE_PATH, new BEValue(mapStringListToBEValueList(torrentFile.relativePath)));
+        files.add(new BEValue(entry));
+      }
+      infoTable.put(FILES, new BEValue(files));
+    }
+
+    mapMetadata.put(INFO_TABLE, new BEValue(infoTable));
+
+    final ByteBuffer buffer = BEncoder.bencode(mapMetadata);
+    return buffer.array();
+  }
+
+  @Nullable
+  private List<BEValue> getAnnounceListAsBEValues(@Nullable List<List<String>> announceList) throws UnsupportedEncodingException {
+    if (announceList == null) return null;
+    List<BEValue> result = new ArrayList<BEValue>();
+
+    for (List<String> announceTier : announceList) {
+      List<BEValue> tier = mapStringListToBEValueList(announceTier);
+      if (!tier.isEmpty()) result.add(new BEValue(tier));
+    }
+
+    if (result.isEmpty()) return null;
+
+    return result;
+  }
+
+  private List<BEValue> mapStringListToBEValueList(List<String> list) throws UnsupportedEncodingException {
+    List<BEValue> result = new ArrayList<BEValue>();
+    for (String s : list) {
+      result.add(new BEValue(s));
+    }
+    return result;
+  }
+
+  private void putOptionalIfPresent(Map<String, BEValue> map, String key, Optional<String> optional) throws UnsupportedEncodingException {
+    if (!optional.isPresent()) return;
+    map.put(key, new BEValue(optional.get()));
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentStatistic.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentStatistic.java
new file mode 100644
index 0000000..8465493
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentStatistic.java
@@ -0,0 +1,72 @@
+package com.turn.ttorrent.common;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Class store statistic for downloaded, uploaded and left bytes count.
+ */
+public class TorrentStatistic {
+
+  private final AtomicLong myUploadedBytes;
+  private final AtomicLong myDownloadedBytes;
+  private final AtomicLong myLeftBytes;
+
+  public TorrentStatistic() {
+    myDownloadedBytes = new AtomicLong();
+    myUploadedBytes = new AtomicLong();
+    myLeftBytes = new AtomicLong();
+  }
+
+  public TorrentStatistic(TorrentStatistic torrentStatistic){
+    myDownloadedBytes = new AtomicLong(torrentStatistic.getDownloadedBytes());
+    myUploadedBytes = new AtomicLong(torrentStatistic.getUploadedBytes());
+    myLeftBytes = new AtomicLong(torrentStatistic.getLeftBytes());
+  }
+
+  public long getUploadedBytes() {
+    return myUploadedBytes.get();
+  }
+
+  public long getDownloadedBytes() {
+    return myDownloadedBytes.get();
+  }
+
+  public long getLeftBytes() {
+    return myLeftBytes.get();
+  }
+
+  public void addUploaded(long delta) {
+    myUploadedBytes.addAndGet(delta);
+  }
+
+  public void addDownloaded(long delta) {
+    myDownloadedBytes.addAndGet(delta);
+  }
+
+  public void addLeft(long delta) {
+    myLeftBytes.addAndGet(delta);
+  }
+
+  public void setLeft(long value) {
+    myLeftBytes.set(value);
+  }
+
+  public void setUploaded(long value) {
+    myUploadedBytes.set(value);
+  }
+
+  public void setDownloaded(long value) {
+    myDownloadedBytes.set(value);
+  }
+
+  public long getPercentageDownloaded(){
+    long downloadedBytes = getDownloadedBytes();
+    long totalBytes = getTotalBytes();
+    return (downloadedBytes * 100) / totalBytes;
+  }
+
+  public long getTotalBytes(){
+    return getDownloadedBytes() + getLeftBytes();
+  }
+
+}
\ No newline at end of file
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentUtils.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentUtils.java
new file mode 100644
index 0000000..c2d6d83
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/TorrentUtils.java
@@ -0,0 +1,50 @@
+package com.turn.ttorrent.common;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class TorrentUtils {
+
+  private final static char[] HEX_SYMBOLS = "0123456789ABCDEF".toCharArray();
+
+  /**
+   * @param data for hashing
+   * @return sha 1 hash of specified data
+   */
+  public static byte[] calculateSha1Hash(byte[] data) {
+    return DigestUtils.sha1(data);
+  }
+
+  /**
+   * Convert a byte string to a string containing an hexadecimal
+   * representation of the original data.
+   *
+   * @param bytes The byte array to convert.
+   */
+  public static String byteArrayToHexString(byte[] bytes) {
+    char[] hexChars = new char[bytes.length * 2];
+    for (int j = 0; j < bytes.length; j++) {
+      int v = bytes[j] & 0xFF;
+      hexChars[j * 2] = HEX_SYMBOLS[v >>> 4];
+      hexChars[j * 2 + 1] = HEX_SYMBOLS[v & 0x0F];
+    }
+    return new String(hexChars);
+  }
+
+  public static boolean isTrackerLessInfo(AnnounceableInformation information) {
+    return information.getAnnounce() == null && information.getAnnounceList() == null;
+  }
+
+  public static List<String> getTorrentFileNames(TorrentMetadata metadata) {
+    List<String> result = new ArrayList<String>();
+
+    for (TorrentFile torrentFile : metadata.getFiles()) {
+      result.add(torrentFile.getRelativePathAsString());
+    }
+
+    return result;
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/CommonHashingCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/CommonHashingCalculator.java
new file mode 100644
index 0000000..006cd17
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/CommonHashingCalculator.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+class CommonHashingCalculator {
+
+  static final CommonHashingCalculator INSTANCE = new CommonHashingCalculator();
+
+  List<Long> processDataSources(List<DataSourceHolder> sources,
+                                int pieceSize,
+                                Processor processor) throws IOException {
+    List<Long> sourcesSizes = new ArrayList<Long>();
+    byte[] buffer = new byte[pieceSize];
+    int read = 0;
+    for (DataSourceHolder source : sources) {
+      long streamSize = 0;
+      InputStream stream = source.getStream();
+      try {
+        while (true) {
+          int readFromStream = stream.read(buffer, read, buffer.length - read);
+          if (readFromStream < 0) {
+            break;
+          }
+          streamSize += readFromStream;
+          read += readFromStream;
+          if (read == buffer.length) {
+            processor.process(buffer);
+            read = 0;
+          }
+        }
+      } finally {
+        source.close();
+        sourcesSizes.add(streamSize);
+      }
+    }
+    if (read > 0) {
+      processor.process(Arrays.copyOf(buffer, read));
+    }
+
+    return sourcesSizes;
+  }
+
+  interface Processor {
+
+    /**
+     * Invoked when next piece is received from data source. Array will be overwritten
+     * after invocation this method (next piece will be read in same array). So multi-threading
+     * implementations must create copy of array and work with the copy.
+     *
+     * @param buffer byte array which contains bytes from data sources.
+     *               length of array equals piece size excluding last piece
+     */
+    void process(byte[] buffer);
+
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/DataSourceHolder.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/DataSourceHolder.java
new file mode 100644
index 0000000..7fb12d8
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/DataSourceHolder.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface DataSourceHolder extends Closeable {
+
+  /**
+   * provides {@link InputStream} associated with the holder. Holder can just store reference to stream or create
+   * new stream from some source (e.g. {@link java.io.FileInputStream} from {@link java.io.File}) on first invocation.
+   *
+   * @return {@link InputStream} associated with the holder.
+   * @throws IOException if io error occurs in creating new stream from source.
+   *                     IO exception can be thrown only on first invocation
+   */
+  InputStream getStream() throws IOException;
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/HashingResult.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/HashingResult.java
new file mode 100644
index 0000000..c97e3b2
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/HashingResult.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import java.util.List;
+
+public class HashingResult {
+
+  private final List<byte[]> hashes;
+  private final List<Long> sourceSizes;
+
+  public HashingResult(List<byte[]> hashes, List<Long> sourceSizes) {
+    this.hashes = hashes;
+    this.sourceSizes = sourceSizes;
+  }
+
+  public List<byte[]> getHashes() {
+    return hashes;
+  }
+
+  public List<Long> getSourceSizes() {
+    return sourceSizes;
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MetadataBuilder.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MetadataBuilder.java
new file mode 100644
index 0000000..71d07da
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MetadataBuilder.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentParser;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.io.*;
+import java.util.*;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+
+@SuppressWarnings({"unused", "WeakerAccess"})
+public class MetadataBuilder {
+
+  private final static Logger logger = TorrentLoggerFactory.getLogger(MetadataBuilder.class);
+  private final static String DEFAULT_CREATED_BY = "ttorrent library";
+
+  //root dictionary
+  @NotNull
+  private String announce = "";
+  @NotNull
+  private List<List<String>> announceList = new ArrayList<List<String>>();
+  private long creationDate = -1;
+  @NotNull
+  private String comment = "";
+  @NotNull
+  private String createdBy = DEFAULT_CREATED_BY;
+  @NotNull
+  private List<String> webSeedUrlList = new ArrayList<String>();
+  //end root dictionary
+
+  //info dictionary
+  private int pieceLength = 512 * 1024;//512kb by default
+  private boolean isPrivate = false;
+  @NotNull
+  private List<String> filesPaths = new ArrayList<String>();
+  @Nullable
+  private HashingResult hashingResult = null;
+  @NotNull
+  private List<DataSourceHolder> dataSources = new ArrayList<DataSourceHolder>();
+  @NotNull
+  private String directoryName = "";
+  //end info dictionary
+
+  //fields which store some internal information
+  @NotNull
+  private PiecesHashesCalculator piecesHashesCalculator = new SingleThreadHashesCalculator();
+  //end
+
+  /**
+   * set main announce tracker URL if you use single tracker.
+   * In case with many trackers use {@link #addTracker(String)}
+   * and {@link #newTier()}. Then as main announce will be selected first tracker.
+   * You can specify main announce using this method for override this behaviour
+   * Torrent clients which support BEP12 extension will ignore main announce.
+   *
+   * @param announce announce URL for the tracker
+   */
+  public MetadataBuilder setTracker(String announce) {
+    this.announce = announce;
+    return this;
+  }
+
+
+  /**
+   * Multi-tracker Metadata Extension. Add new tracker URL to current tier.
+   * This method will create first tier automatically if it doesn't exist
+   * You can find more information about this extension in documentation
+   * <a href="http://bittorrent.org/beps/bep_0012.html">http://bittorrent.org/beps/bep_0012.html</a>
+   *
+   * @param url tracker url
+   */
+  public MetadataBuilder addTracker(String url) {
+    initFirstTier();
+    announceList.get(announceList.size() - 1).add(url);
+    return this;
+  }
+
+  /**
+   * Multi-tracker Metadata Extension. Add all trackers to current tier.
+   * This method will create first tier automatically if it doesn't exist
+   * You can find more information about this extension in documentation
+   * <a href="http://bittorrent.org/beps/bep_0012.html">http://bittorrent.org/beps/bep_0012.html</a>
+   *
+   * @param trackers collections of trackers URLs
+   */
+  public MetadataBuilder addTrackers(Collection<String> trackers) {
+    initFirstTier();
+    announceList.get(announceList.size() - 1).addAll(trackers);
+    return this;
+  }
+
+  /**
+   * Multi-tracker Metadata Extension. Create new tier for adding tracker using {@link #addTracker(String)} method
+   * If you don't add at least one tracker on the tier this tier will be removed in building metadata
+   * You can find more information about this extension in documentation
+   * <a href="http://bittorrent.org/beps/bep_0012.html">http://bittorrent.org/beps/bep_0012.html</a>
+   */
+  public MetadataBuilder newTier() {
+    announceList.add(new ArrayList<String>());
+    return this;
+  }
+
+  /**
+   * Web Seeding Metadata.
+   * Web seeding url as defined by <a href='http://bittorrent.org/beps/bep_0019.html'>bep 0019</a>
+   * @param url URL to add for web seeding
+   */
+  public MetadataBuilder addWebSeedUrl(String url)  {
+    webSeedUrlList.add(url);
+    return this;
+  }
+
+  /**
+   * Set the creation time of the torrent in standard UNIX epoch format.
+   *
+   * @param creationTime the seconds since January 1, 1970, 00:00:00 UTC.
+   */
+  public MetadataBuilder setCreationTime(int creationTime) {
+    this.creationDate = creationTime;
+    return this;
+  }
+
+  /**
+   * Set free-form textual comment of the author
+   */
+  public MetadataBuilder setComment(String comment) {
+    this.comment = comment;
+    return this;
+  }
+
+  /**
+   * Set program name which is used for creating torrent file.
+   */
+  public MetadataBuilder setCreatedBy(String createdBy) {
+    this.createdBy = createdBy;
+    return this;
+  }
+
+  /**
+   * Set {@link PiecesHashesCalculator} instance for calculating hashes. In rare cases user's
+   * implementation can be used for increasing hashing performance
+   */
+  public MetadataBuilder setPiecesHashesCalculator(@NotNull PiecesHashesCalculator piecesHashesCalculator) {
+    this.piecesHashesCalculator = piecesHashesCalculator;
+    return this;
+  }
+
+  /**
+   * Set length int bytes of one piece. By default is used 512KB.
+   * Larger piece size reduces size of .torrent file but cause inefficiency
+   * (torrent-client need to download full piece from peer for validating)
+   * and too-small piece sizes cause large .torrent metadata file.
+   * Recommended size is between 256KB and 1MB.
+   */
+  public MetadataBuilder setPieceLength(int pieceLength) {
+    this.pieceLength = pieceLength;
+    return this;
+  }
+
+  /**
+   * Set the name of the directory in which to store all the files.
+   * If {@link #directoryName} isn't empty then multi-file torrent will be created, otherwise single-file
+   */
+  public MetadataBuilder setDirectoryName(@NotNull String directoryName) {
+    this.directoryName = directoryName;
+    return this;
+  }
+
+  /**
+   * add custom source in torrent with custom path. Path can be separated with any slash.
+   *
+   * @param closeAfterBuild if true then source stream will be closed after {@link #build()} invocation
+   */
+  public MetadataBuilder addDataSource(@NotNull InputStream dataSource, String path, boolean closeAfterBuild) {
+    checkHashingResultIsNotSet();
+    filesPaths.add(path);
+    dataSources.add(new StreamBasedHolderImpl(dataSource, closeAfterBuild));
+    return this;
+  }
+
+  /**
+   * add custom source in torrent with custom path. Path can be separated with any slash.
+   */
+  public MetadataBuilder addDataSource(@NotNull InputStream dataSource, String path) {
+    addDataSource(dataSource, path, true);
+    return this;
+  }
+
+  /**
+   * add specified file in torrent with custom path. The file will be stored in .torrent
+   * by specified path. Path can be separated with any slash. In case of single-file torrent
+   * this path will be used as name of source file
+   */
+  public MetadataBuilder addFile(@NotNull File source, @NotNull String path) {
+    if (!source.isFile()) {
+      throw new IllegalArgumentException(source + " is not exist");
+    }
+    checkHashingResultIsNotSet();
+    filesPaths.add(path);
+    dataSources.add(new FileSourceHolder(source));
+    return this;
+  }
+
+  private void checkHashingResultIsNotSet() {
+    if (hashingResult != null) {
+      throw new IllegalStateException("Unable to add new source when hashes are set manually");
+    }
+  }
+
+  /**
+   * add specified file in torrent. In case of multi-torrent this file will be downloaded to
+   * {@link #directoryName}. In single-file torrent this file will be downloaded in download folder
+   */
+  public MetadataBuilder addFile(@NotNull File source) {
+    return addFile(source, source.getName());
+  }
+
+  /**
+   * allow to create information about files via speicified hashes, files paths and files lengths.
+   * Using of this method is not compatible with using source-based methods
+   * ({@link #addFile(File)}, {@link #addDataSource(InputStream, String, boolean)}, etc
+   * because it's not possible to calculate concat this hashes and calculated hashes.
+   * each byte array in hashes list should have {{@link Constants#PIECE_HASH_SIZE}} length
+   *
+   * @param hashes       list of files hashes in same order as files in files paths list
+   * @param filesPaths   list of files paths
+   * @param filesLengths list of files lengths in same order as files in files paths list
+   */
+  public MetadataBuilder setFilesInfo(@NotNull List<byte[]> hashes,
+                                      @NotNull List<String> filesPaths,
+                                      @NotNull List<Long> filesLengths) {
+    if (dataSources.size() != 0) {
+      throw new IllegalStateException("Unable to add hashes-based files info. Some data sources already added");
+    }
+    this.filesPaths.clear();
+    this.filesPaths.addAll(filesPaths);
+    this.hashingResult = new HashingResult(hashes, filesLengths);
+    return this;
+  }
+
+  /**
+   * marks torrent as private
+   *
+   * @see <a href="http://bittorrent.org/beps/bep_0027.html">http://bittorrent.org/beps/bep_0027.html</a>
+   */
+  public void doPrivate() {
+    isPrivate = true;
+  }
+
+  /**
+   * marks torrent as public
+   *
+   * @see <a href="http://bittorrent.org/beps/bep_0027.html">http://bittorrent.org/beps/bep_0027.html</a>
+   */
+  public void doPublic() {
+    isPrivate = false;
+  }
+
+  /**
+   * @return new {@link TorrentMetadata} instance with builder's fields
+   * @throws IOException           if IO error occurs on reading from source streams and files
+   * @throws IllegalStateException if builder's state is incorrect (e.g. missing required fields)
+   */
+  public TorrentMetadata build() throws IOException {
+    return new TorrentParser().parse(buildBinary());
+  }
+
+  /**
+   * @return binary representation of metadata
+   * @throws IOException           if IO error occurs on reading from source streams and files
+   * @throws IllegalStateException if builder's state is incorrect (e.g. missing required fields)
+   */
+  public byte[] buildBinary() throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    BEncoder.bencode(buildBEP(), out);
+    return out.toByteArray();
+  }
+
+  /**
+   * @return BEP-encoded dictionary of metadata
+   * @throws IOException           if IO error occurs on reading from source streams and files
+   * @throws IllegalStateException if builder's state is incorrect (e.g. missing required fields)
+   */
+  public BEValue buildBEP() throws IOException {
+    return buildAndCloseResources();
+  }
+
+  private BEValue buildAndCloseResources() throws IOException {
+    try {
+      return doBuild();
+    } finally {
+      closeAllSources();
+    }
+  }
+
+  private BEValue doBuild() throws IOException {
+    dropEmptyTiersFromAnnounce();
+
+    if (announce.isEmpty() && !announceList.isEmpty()) {
+        announce = announceList.get(0).get(0);
+    }
+    if (filesPaths.size() == 0) {
+      throw new IllegalStateException("Unable to create metadata without sources. Use addSource() method for adding sources");
+    }
+    final boolean isSingleMode = filesPaths.size() == 1 && directoryName.isEmpty();
+    final String name;
+    if (!directoryName.isEmpty()) {
+      name = directoryName;
+    } else {
+      if (isSingleMode) {
+        name = filesPaths.get(0);
+      } else {
+        throw new IllegalStateException("Missing required field 'name'. Use setDirectoryName() method for specifying name of torrent");
+      }
+    }
+
+    Map<String, BEValue> torrent = new HashMap<String, BEValue>();
+    if (!announce.isEmpty()) torrent.put(ANNOUNCE, new BEValue(announce));
+    if (!announceList.isEmpty()) torrent.put(ANNOUNCE_LIST, wrapAnnounceList());
+    if (creationDate > 0) {
+      torrent.put(CREATION_DATE_SEC, new BEValue(creationDate));
+    }
+
+    if (!comment.isEmpty()) torrent.put(COMMENT, new BEValue(comment));
+    if (!createdBy.isEmpty()) torrent.put(CREATED_BY, new BEValue(createdBy));
+    if (!webSeedUrlList.isEmpty()) torrent.put(URL_LIST, wrapStringList(webSeedUrlList));
+
+    HashingResult hashingResult = this.hashingResult == null ?
+            piecesHashesCalculator.calculateHashes(dataSources, pieceLength) :
+            this.hashingResult;
+
+    Map<String, BEValue> info = new HashMap<String, BEValue>();
+    info.put(PIECE_LENGTH, new BEValue(pieceLength));
+    info.put(PIECES, concatHashes(hashingResult.getHashes()));
+    info.put(PRIVATE, new BEValue(isPrivate ? 1 : 0));
+    info.put(NAME, new BEValue(name));
+    if (isSingleMode) {
+      Long sourceSize = hashingResult.getSourceSizes().get(0);
+      info.put(FILE_LENGTH, new BEValue(sourceSize));
+    } else {
+      List<BEValue> files = getFilesList(hashingResult);
+      info.put(FILES, new BEValue(files));
+    }
+    torrent.put(INFO_TABLE, new BEValue(info));
+
+    return new BEValue(torrent);
+  }
+
+  private List<BEValue> getFilesList(HashingResult hashingResult) throws UnsupportedEncodingException {
+    ArrayList<BEValue> result = new ArrayList<BEValue>();
+    for (int i = 0; i < filesPaths.size(); i++) {
+      Map<String, BEValue> file = new HashMap<String, BEValue>();
+      Long sourceSize = hashingResult.getSourceSizes().get(i);
+      String fullPath = filesPaths.get(i);
+      List<BEValue> filePath = new ArrayList<BEValue>();
+      for (String path : fullPath.replace("\\", "/").split("/")) {
+        filePath.add(new BEValue(path));
+      }
+      file.put(FILE_PATH, new BEValue(filePath));
+      file.put(FILE_LENGTH, new BEValue(sourceSize));
+      result.add(new BEValue(file));
+    }
+    return result;
+  }
+
+  private BEValue concatHashes(List<byte[]> hashes) throws UnsupportedEncodingException {
+    StringBuilder sb = new StringBuilder();
+    for (byte[] hash : hashes) {
+      sb.append(new String(hash, Constants.BYTE_ENCODING));
+    }
+    return new BEValue(sb.toString(), Constants.BYTE_ENCODING);
+  }
+
+  private BEValue wrapStringList(List<String> lst) throws UnsupportedEncodingException {
+    List<BEValue> result = new LinkedList<BEValue>();
+    for(String s : lst) {
+      result.add(new BEValue(s));
+    }
+    return new BEValue(result);
+  }
+
+  private BEValue wrapAnnounceList() throws UnsupportedEncodingException {
+    List<BEValue> result = new LinkedList<BEValue>();
+    for (List<String> tier : announceList) {
+      result.add(wrapStringList(tier));
+    }
+    return new BEValue(result);
+  }
+
+  private void dropEmptyTiersFromAnnounce() {
+    Iterator<List<String>> iterator = announceList.iterator();
+    while (iterator.hasNext()) {
+      List<String> tier = iterator.next();
+      if (tier.isEmpty()) {
+        iterator.remove();
+      }
+    }
+  }
+
+  private void closeAllSources() {
+    for (DataSourceHolder sourceHolder : dataSources) {
+      try {
+        sourceHolder.close();
+      } catch (Throwable e) {
+        logger.error("Error in closing data source " + sourceHolder, e);
+      }
+    }
+  }
+
+  private void initFirstTier() {
+    if (announceList.isEmpty()) {
+      newTier();
+    }
+  }
+
+  private static class FileSourceHolder implements DataSourceHolder {
+    @Nullable
+    private FileInputStream fis;
+    @NotNull
+    private final File source;
+
+    public FileSourceHolder(@NotNull File source) {
+      this.source = source;
+    }
+
+    @Override
+    public InputStream getStream() throws IOException {
+      if (fis == null) {
+        fis = new FileInputStream(source);
+      }
+      return fis;
+    }
+
+    @Override
+    public void close() throws IOException {
+      if (fis != null) {
+        fis.close();
+      }
+    }
+
+    @Override
+    public String toString() {
+      return "Data source for file stream " + fis;
+    }
+  }
+
+  private static class StreamBasedHolderImpl implements DataSourceHolder {
+    private final InputStream source;
+    private final boolean closeAfterBuild;
+
+    public StreamBasedHolderImpl(InputStream source, boolean closeAfterBuild) {
+      this.source = source;
+      this.closeAfterBuild = closeAfterBuild;
+    }
+
+    @Override
+    public InputStream getStream() {
+      return source;
+    }
+
+    @Override
+    public void close() throws IOException {
+      if (closeAfterBuild) {
+        source.close();
+      }
+    }
+
+    @Override
+    public String toString() {
+      return "Data source for user's stream " + source;
+    }
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MultiThreadHashesCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MultiThreadHashesCalculator.java
new file mode 100644
index 0000000..cd6b4b2
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/MultiThreadHashesCalculator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import com.turn.ttorrent.common.TorrentUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class MultiThreadHashesCalculator implements PiecesHashesCalculator {
+
+  private final ExecutorService executor;
+  private final int maxInMemoryPieces;
+
+  public MultiThreadHashesCalculator(ExecutorService executor, int maxInMemoryPieces) {
+    this.executor = executor;
+    this.maxInMemoryPieces = maxInMemoryPieces;
+  }
+
+  @Override
+  public HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) throws IOException {
+    final List<byte[]> hashes = new ArrayList<byte[]>();
+    final List<Future<byte[]>> futures = new ArrayList<Future<byte[]>>();
+    List<Long> sourcesSizes = CommonHashingCalculator.INSTANCE.processDataSources(
+            sources,
+            pieceSize,
+            new CommonHashingCalculator.Processor() {
+              @Override
+              public void process(final byte[] buffer) {
+                awaitHashesCalculationAndStore(futures, hashes, maxInMemoryPieces);
+                final byte[] bufferCopy = Arrays.copyOf(buffer, buffer.length);
+                futures.add(executor.submit(new Callable<byte[]>() {
+                  @Override
+                  public byte[] call() {
+                    return TorrentUtils.calculateSha1Hash(bufferCopy);
+                  }
+                }));
+              }
+            }
+    );
+    awaitHashesCalculationAndStore(futures, hashes, 0);
+
+    return new HashingResult(hashes, sourcesSizes);
+  }
+
+  private void awaitHashesCalculationAndStore(List<Future<byte[]>> futures, List<byte[]> hashes, int count) {
+    while (futures.size() > count) {
+      byte[] hash;
+      try {
+        Future<byte[]> future = futures.remove(0);
+        hash = future.get();
+      } catch (InterruptedException e) {
+        throw new RuntimeException(e);
+      } catch (ExecutionException e) {
+        throw new RuntimeException(e);
+      }
+      hashes.add(hash);
+    }
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/PiecesHashesCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/PiecesHashesCalculator.java
new file mode 100644
index 0000000..dda7396
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/PiecesHashesCalculator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface PiecesHashesCalculator {
+
+  /**
+   * calculates sha1 hashes of each chunk with specified piece size
+   * and returns list of hashes and stream's sizes. If one stream is ended and piece size threshold is not reached
+   * implementation must read bytes from next stream
+   * For example if source list is 3 streams with next bytes:
+   * first stream: [1,2,3]
+   * second stream: [4,5,6,7]
+   * third stream: [8,9]
+   * and pieceSize = 4
+   * result must contain source size [3,4,2] and hashes: [sha1(1,2,3,4), sha1(5,6,7,8), sha1(9)]
+   *
+   * @param sources   list of input stream's providers
+   * @param pieceSize size of one piece
+   * @return see above
+   * @throws IOException if IO error occurs in reading from streams
+   */
+  HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) throws IOException;
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/SingleThreadHashesCalculator.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/SingleThreadHashesCalculator.java
new file mode 100644
index 0000000..0446372
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/SingleThreadHashesCalculator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import com.turn.ttorrent.common.TorrentUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SingleThreadHashesCalculator implements PiecesHashesCalculator {
+
+  @Override
+  public HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) throws IOException {
+    final List<byte[]> hashes = new ArrayList<byte[]>();
+    List<Long> sourcesSizes = CommonHashingCalculator.INSTANCE.processDataSources(
+            sources,
+            pieceSize,
+            new CommonHashingCalculator.Processor() {
+              @Override
+              public void process(byte[] buffer) {
+                byte[] hash = TorrentUtils.calculateSha1Hash(buffer);
+                hashes.add(hash);
+              }
+            }
+    );
+
+    return new HashingResult(hashes, sourcesSizes);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/StringUtils.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/StringUtils.java
new file mode 100644
index 0000000..dfe8c8c
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/creation/StringUtils.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import java.util.Iterator;
+
+public final class StringUtils {
+
+  public static String join(String delimiter, Iterable<? extends CharSequence> iterable) {
+    Iterator<? extends CharSequence> iterator = iterable.iterator();
+    StringBuilder sb = new StringBuilder();
+    if (iterator.hasNext()) {
+      sb.append(iterator.next());
+    }
+    while (iterator.hasNext()) {
+      sb.append(delimiter).append(iterator.next());
+    }
+    return sb.toString();
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceRequestMessage.java
new file mode 100644
index 0000000..92a9110
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceRequestMessage.java
@@ -0,0 +1,171 @@
+package com.turn.ttorrent.common.protocol;
+
+/**
+ * Base interface for announce request messages.
+ * 公告请求消息的基础接口。
+ *
+ * <p>
+ * This interface must be implemented by all subtypes of announce request
+ * messages for the various tracker protocols.
+ * </p>
+ *
+ * For details information see <a href="https://wiki.theory.org/index.php/BitTorrentSpecification#Tracker_Request_Parameters"></a>
+ *
+ * @author mpetazzoni
+ */
+public interface AnnounceRequestMessage {
+
+  int DEFAULT_NUM_WANT = 50;
+
+  /**
+   * Announce request event types.
+   *
+   * <p>
+   * When the client starts exchanging on a torrent, it must contact the
+   * torrent's tracker with a 'started' announce request, which notifies the
+   * tracker this client now exchanges on this torrent (and thus allows the
+   * tracker to report the existence of this peer to other clients).
+   * </p>
+   *
+   * <p>
+   * When the client stops exchanging, or when its download completes, it must
+   * also send a specific announce request. Otherwise, the client must send an
+   * eventless (NONE), periodic announce request to the tracker at an
+   * interval specified by the tracker itself, allowing the tracker to
+   * refresh this peer's status and acknowledge that it is still there.
+   * </p>
+   */
+  enum RequestEvent {
+    NONE(0),
+    COMPLETED(1),
+    STARTED(2),
+    STOPPED(3);
+
+    private final int id;
+
+    RequestEvent(int id) {
+      this.id = id;
+    }
+
+    public String getEventName() {
+      return this.name().toLowerCase();
+    }
+
+    public int getId() {
+      return this.id;
+    }
+
+    public static RequestEvent getByName(String name) {
+      for (RequestEvent type : RequestEvent.values()) {
+        if (type.name().equalsIgnoreCase(name)) {
+          return type;
+        }
+      }
+      return null;
+    }
+
+    public static RequestEvent getById(int id) {
+      for (RequestEvent type : RequestEvent.values()) {
+        if (type.getId() == id) {
+          return type;
+        }
+      }
+      return null;
+    }
+  }
+
+  /**
+   * @return SHA1 hash of value associated with "info" key in .torrent file
+   */
+  byte[] getInfoHash();
+
+  /**
+   * String representation of {@link #getInfoHash} where each byte replaced by hex-string value with lead zero
+   * for example for byte array [1, 2, 15, -2] this method must return 01020FFE
+   *
+   * @return String representation of {@link #getInfoHash}
+   */
+  String getHexInfoHash();
+
+  /**
+   * @return peer id generated by current client
+   */
+  byte[] getPeerId();
+
+  /**
+   * @return String representation of {@link #getPeerId}. It's similarly {@link #getHexInfoHash()} method
+   */
+  String getHexPeerId();
+
+  /**
+   * @return current client port on which it listens for new connections
+   */
+  int getPort();
+
+  /**
+   * @return count of uploaded bytes for current torrent by current peer after sending STARTED event to the tracker
+   */
+  long getUploaded();
+
+  /**
+   * @return count of downloaded bytes for current torrent by current peer after sending STARTED event to the tracker
+   */
+  long getDownloaded();
+
+  /**
+   * @return count of bytes which client must be download
+   */
+  long getLeft();
+
+  /**
+   * Tells that it's compact request.
+   * In this case tracker return compact peers list which contains only address and port without peer id,
+   * but according to specification some trackers can ignore this parameter
+   *
+   * @return true if it's compact request.
+   */
+  boolean isCompact();
+
+  /**
+   * Tells that tracker can omit peer id field in response. This parameter is ignored if {@link #isCompact()} method
+   * return true
+   *
+   * @return true if tracker can omit peer id
+   */
+  boolean canOmitPeerId();
+
+  /**
+   * @return event of current request
+   */
+  RequestEvent getEvent();
+
+  /**
+   * Optional. If it's not specified thet tracker get ip address from request
+   *
+   * @return current client address on which it listens for new connections
+   */
+  String getIp();
+
+  /**
+   * Optional. If it's not specified (value is zero or negative) tracker return default peers count. As a rule this count is 50
+   *
+   * @return count of peers which client want to get from tracker
+   */
+  int getNumWant();
+
+  /**
+   * Optional. Contains key of current client. Client can use this key for confirm for tracker that it's old client
+   * after change IP address
+   *
+   * @return key of current client.
+   */
+  String getKey();
+
+  /**
+   * Optional. If previous response from tracker contains tracker id field, then client must send this value here
+   *
+   * @return previous tracker id
+   */
+  String getTrackerId();
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceResponseMessage.java
new file mode 100644
index 0000000..89bf212
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/AnnounceResponseMessage.java
@@ -0,0 +1,26 @@
+package com.turn.ttorrent.common.protocol;
+
+import com.turn.ttorrent.common.Peer;
+
+import java.util.List;
+
+/**
+ * Base interface for announce response messages.
+ *
+ * <p>
+ * This interface must be implemented by all subtypes of announce response
+ * messages for the various tracker protocols.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public interface AnnounceResponseMessage {
+
+  int getInterval();
+
+  int getComplete();
+
+  int getIncomplete();
+
+  List<Peer> getPeers();
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java
new file mode 100644
index 0000000..6ffba62
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/PeerMessage.java
@@ -0,0 +1,692 @@
+/**
+ * Copyright (C) 2011-2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.common.TorrentInfo;
+
+import java.nio.ByteBuffer;
+import java.text.ParseException;
+import java.util.BitSet;
+
+/**
+ * BitTorrent peer protocol messages representations.
+ *
+ * <p>
+ * This class and its <em>*Messages</em> subclasses provide POJO
+ * representations of the peer protocol messages, along with easy parsing from
+ * an input ByteBuffer to quickly get a usable representation of an incoming
+ * message.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @see <a href="http://wiki.theory.org/BitTorrentSpecification#Peer_wire_protocol_.28TCP.29">BitTorrent peer wire protocol</a>
+ */
+public abstract class PeerMessage {
+
+  /**
+   * The size, in bytes, of the length field in a message (one 32-bit
+   * integer).
+   */
+  public static final int MESSAGE_LENGTH_FIELD_SIZE = 4;
+
+  /**
+   * Message type.
+   *
+   * <p>
+   * Note that the keep-alive messages don't actually have an type ID defined
+   * in the protocol as they are of length 0.
+   * </p>
+   */
+  public enum Type {
+    KEEP_ALIVE(-1),
+    CHOKE(0),
+    UNCHOKE(1),
+    INTERESTED(2),
+    NOT_INTERESTED(3),
+    HAVE(4),
+    BITFIELD(5),
+    REQUEST(6),
+    PIECE(7),
+    CANCEL(8);
+
+    private byte id;
+
+    Type(int id) {
+      this.id = (byte) id;
+    }
+
+    public boolean equals(byte c) {
+      return this.id == c;
+    }
+
+    public byte getTypeByte() {
+      return this.id;
+    }
+
+    public static Type get(byte c) {
+      for (Type t : Type.values()) {
+        if (t.equals(c)) {
+          return t;
+        }
+      }
+      return null;
+    }
+  }
+
+  private final Type type;
+  private final ByteBuffer data;
+
+  private PeerMessage(Type type, ByteBuffer data) {
+    this.type = type;
+    this.data = data;
+    this.data.rewind();
+  }
+
+  public Type getType() {
+    return this.type;
+  }
+
+  /**
+   * Returns a {@link ByteBuffer} backed by the same data as this message.
+   *
+   * <p>
+   * This method returns a duplicate of the buffer stored in this {@link
+   * PeerMessage} object to allow for multiple consumers to read from the
+   * same message without conflicting access to the buffer's position, mark
+   * and limit.
+   * </p>
+   */
+  public ByteBuffer getData() {
+    return this.data.duplicate();
+  }
+
+  /**
+   * Validate that this message makes sense for the torrent it's related to.
+   *
+   * <p>
+   * This method is meant to be overloaded by distinct message types, where
+   * it makes sense. Otherwise, it defaults to true.
+   * </p>
+   *
+   * @param torrent The torrent this message is about.
+   */
+  public PeerMessage validate(TorrentInfo torrent)
+          throws MessageValidationException {
+    return this;
+  }
+
+  public String toString() {
+    return this.getType().name();
+  }
+
+  /**
+   * Parse the given buffer into a peer protocol message.
+   *
+   * <p>
+   * Parses the provided byte array and builds the corresponding PeerMessage
+   * subclass object.
+   * </p>
+   *
+   * @param buffer  The byte buffer containing the message data.
+   * @param torrent The torrent this message is about.
+   * @return A PeerMessage subclass instance.
+   * @throws ParseException When the message is invalid, can't be parsed or
+   *                        does not match the protocol requirements.
+   */
+  public static PeerMessage parse(ByteBuffer buffer, TorrentInfo torrent)
+          throws ParseException {
+    int length = buffer.getInt();
+    if (length == 0) {
+      return KeepAliveMessage.parse(buffer, torrent);
+    } else if (length != buffer.remaining()) {
+      throw new ParseException("Message size did not match announced " +
+              "size!", 0);
+    }
+
+    Type type = Type.get(buffer.get());
+    if (type == null) {
+      throw new ParseException("Unknown message ID!",
+              buffer.position() - 1);
+    }
+
+    switch (type) {
+      case CHOKE:
+        return ChokeMessage.parse(buffer.slice(), torrent);
+      case UNCHOKE:
+        return UnchokeMessage.parse(buffer.slice(), torrent);
+      case INTERESTED:
+        return InterestedMessage.parse(buffer.slice(), torrent);
+      case NOT_INTERESTED:
+        return NotInterestedMessage.parse(buffer.slice(), torrent);
+      case HAVE:
+        return HaveMessage.parse(buffer.slice(), torrent);
+      case BITFIELD:
+        return BitfieldMessage.parse(buffer.slice(), torrent);
+      case REQUEST:
+        return RequestMessage.parse(buffer.slice(), torrent);
+      case PIECE:
+        return PieceMessage.parse(buffer.slice(), torrent);
+      case CANCEL:
+        return CancelMessage.parse(buffer.slice(), torrent);
+      default:
+        throw new IllegalStateException("Message type should have " +
+                "been properly defined by now.");
+    }
+  }
+
+  public static class MessageValidationException extends ParseException {
+
+    static final long serialVersionUID = -1;
+
+    public MessageValidationException(PeerMessage m) {
+      super("Message " + m + " is not valid!", 0);
+    }
+
+  }
+
+
+  /**
+   * Keep alive message.
+   *
+   * <len=0000>
+   */
+  public static class KeepAliveMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 0;
+
+    private KeepAliveMessage(ByteBuffer buffer) {
+      super(Type.KEEP_ALIVE, buffer);
+    }
+
+    public static KeepAliveMessage parse(ByteBuffer buffer,
+                                         TorrentInfo torrent) throws MessageValidationException {
+      return (KeepAliveMessage) new KeepAliveMessage(buffer)
+              .validate(torrent);
+    }
+
+    public static KeepAliveMessage craft() {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + KeepAliveMessage.BASE_SIZE);
+      buffer.putInt(KeepAliveMessage.BASE_SIZE);
+      return new KeepAliveMessage(buffer);
+    }
+  }
+
+  /**
+   * Choke message.
+   *
+   * <len=0001><id=0>
+   */
+  public static class ChokeMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 1;
+
+    private ChokeMessage(ByteBuffer buffer) {
+      super(Type.CHOKE, buffer);
+    }
+
+    public static ChokeMessage parse(ByteBuffer buffer,
+                                     TorrentInfo torrent) throws MessageValidationException {
+      return (ChokeMessage) new ChokeMessage(buffer)
+              .validate(torrent);
+    }
+
+    public static ChokeMessage craft() {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + ChokeMessage.BASE_SIZE);
+      buffer.putInt(ChokeMessage.BASE_SIZE);
+      buffer.put(PeerMessage.Type.CHOKE.getTypeByte());
+      return new ChokeMessage(buffer);
+    }
+  }
+
+  /**
+   * Unchoke message.
+   *
+   * <len=0001><id=1>
+   */
+  public static class UnchokeMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 1;
+
+    private UnchokeMessage(ByteBuffer buffer) {
+      super(Type.UNCHOKE, buffer);
+    }
+
+    public static UnchokeMessage parse(ByteBuffer buffer,
+                                       TorrentInfo torrent) throws MessageValidationException {
+      return (UnchokeMessage) new UnchokeMessage(buffer)
+              .validate(torrent);
+    }
+
+    public static UnchokeMessage craft() {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + UnchokeMessage.BASE_SIZE);
+      buffer.putInt(UnchokeMessage.BASE_SIZE);
+      buffer.put(PeerMessage.Type.UNCHOKE.getTypeByte());
+      return new UnchokeMessage(buffer);
+    }
+  }
+
+  /**
+   * Interested message.
+   *
+   * <len=0001><id=2>
+   */
+  public static class InterestedMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 1;
+
+    private InterestedMessage(ByteBuffer buffer) {
+      super(Type.INTERESTED, buffer);
+    }
+
+    public static InterestedMessage parse(ByteBuffer buffer,
+                                          TorrentInfo torrent) throws MessageValidationException {
+      return (InterestedMessage) new InterestedMessage(buffer)
+              .validate(torrent);
+    }
+
+    public static InterestedMessage craft() {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + InterestedMessage.BASE_SIZE);
+      buffer.putInt(InterestedMessage.BASE_SIZE);
+      buffer.put(PeerMessage.Type.INTERESTED.getTypeByte());
+      return new InterestedMessage(buffer);
+    }
+  }
+
+  /**
+   * Not interested message.
+   *
+   * <len=0001><id=3>
+   */
+  public static class NotInterestedMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 1;
+
+    private NotInterestedMessage(ByteBuffer buffer) {
+      super(Type.NOT_INTERESTED, buffer);
+    }
+
+    public static NotInterestedMessage parse(ByteBuffer buffer,
+                                             TorrentInfo torrent) throws MessageValidationException {
+      return (NotInterestedMessage) new NotInterestedMessage(buffer)
+              .validate(torrent);
+    }
+
+    public static NotInterestedMessage craft() {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + NotInterestedMessage.BASE_SIZE);
+      buffer.putInt(NotInterestedMessage.BASE_SIZE);
+      buffer.put(PeerMessage.Type.NOT_INTERESTED.getTypeByte());
+      return new NotInterestedMessage(buffer);
+    }
+  }
+
+  /**
+   * Have message.
+   *
+   * <len=0005><id=4><piece index=xxxx>
+   */
+  public static class HaveMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 5;
+
+    private int piece;
+
+    private HaveMessage(ByteBuffer buffer, int piece) {
+      super(Type.HAVE, buffer);
+      this.piece = piece;
+    }
+
+    public int getPieceIndex() {
+      return this.piece;
+    }
+
+    @Override
+    public HaveMessage validate(TorrentInfo torrent)
+            throws MessageValidationException {
+      if (this.piece >= 0 && this.piece < torrent.getPieceCount()) {
+        return this;
+      }
+
+      throw new MessageValidationException(this);
+    }
+
+    public static HaveMessage parse(ByteBuffer buffer,
+                                    TorrentInfo torrent) throws MessageValidationException {
+      return new HaveMessage(buffer, buffer.getInt())
+              .validate(torrent);
+    }
+
+    public static HaveMessage craft(int piece) {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + HaveMessage.BASE_SIZE);
+      buffer.putInt(HaveMessage.BASE_SIZE);
+      buffer.put(PeerMessage.Type.HAVE.getTypeByte());
+      buffer.putInt(piece);
+      return new HaveMessage(buffer, piece);
+    }
+
+    public String toString() {
+      return super.toString() + " #" + this.getPieceIndex();
+    }
+  }
+
+  /**
+   * Bitfield message.
+   *
+   * <len=0001+X><id=5><bitfield>
+   */
+  public static class BitfieldMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 1;
+
+    private BitSet bitfield;
+
+    private BitfieldMessage(ByteBuffer buffer, BitSet bitfield) {
+      super(Type.BITFIELD, buffer);
+      this.bitfield = bitfield;
+    }
+
+    public BitSet getBitfield() {
+      return this.bitfield;
+    }
+
+    @Override
+    public BitfieldMessage validate(TorrentInfo torrent)
+            throws MessageValidationException {
+      if (this.bitfield.length() <= torrent.getPieceCount()) {
+        return this;
+      }
+
+      throw new MessageValidationException(this);
+    }
+
+    public static BitfieldMessage parse(ByteBuffer buffer,
+                                        TorrentInfo torrent) throws MessageValidationException {
+      BitSet bitfield = new BitSet(buffer.remaining() * 8);
+      for (int i = 0; i < buffer.remaining() * 8; i++) {
+        if ((buffer.get(i / 8) & (1 << (7 - (i % 8)))) > 0) {
+          bitfield.set(i);
+        }
+      }
+
+      return new BitfieldMessage(buffer, bitfield)
+              .validate(torrent);
+    }
+
+    public static BitfieldMessage craft(BitSet availablePieces) {
+      int len = availablePieces.length() / 8;
+      if (availablePieces.length() % 8 > 0) len++;
+      byte[] bitfield = new byte[len];
+      for (int i = availablePieces.nextSetBit(0); i >= 0;
+           i = availablePieces.nextSetBit(i + 1)) {
+        bitfield[i / 8] |= 1 << (7 - (i % 8));
+      }
+
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + BitfieldMessage.BASE_SIZE + bitfield.length);
+      buffer.putInt(BitfieldMessage.BASE_SIZE + bitfield.length);
+      buffer.put(PeerMessage.Type.BITFIELD.getTypeByte());
+      buffer.put(ByteBuffer.wrap(bitfield));
+      return new BitfieldMessage(buffer, availablePieces);
+    }
+
+    public String toString() {
+      return super.toString() + " " + this.getBitfield().cardinality();
+    }
+  }
+
+  /**
+   * Request message.
+   *
+   * <len=00013><id=6><piece index><block offset><block length>
+   */
+  public static class RequestMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 13;
+
+    /**
+     * Default block size is 2^14 bytes, or 16kB.
+     */
+    public static final int DEFAULT_REQUEST_SIZE = 16384;
+
+    /**
+     * Max block request size is 2^17 bytes, or 131kB.
+     */
+    public static final int MAX_REQUEST_SIZE = 131072;
+
+    private int piece;
+    private int offset;
+    private int length;
+    private long mySendTime;
+
+    private RequestMessage(ByteBuffer buffer, int piece,
+                           int offset, int length) {
+      super(Type.REQUEST, buffer);
+      this.piece = piece;
+      this.offset = offset;
+      this.length = length;
+      mySendTime = System.currentTimeMillis();
+    }
+
+    public int getPiece() {
+      return this.piece;
+    }
+
+    public int getOffset() {
+      return this.offset;
+    }
+
+    public int getLength() {
+      return this.length;
+    }
+
+    public long getSendTime() {
+      return mySendTime;
+    }
+
+    public void renew() {
+      mySendTime = System.currentTimeMillis();
+    }
+
+    @Override
+    public RequestMessage validate(TorrentInfo torrent)
+            throws MessageValidationException {
+      if (this.piece >= 0 && this.piece < torrent.getPieceCount() &&
+              this.offset + this.length <=
+                      torrent.getPieceSize(this.piece)) {
+        return this;
+      }
+
+      throw new MessageValidationException(this);
+    }
+
+    public static RequestMessage parse(ByteBuffer buffer,
+                                       TorrentInfo torrent) throws MessageValidationException {
+      int piece = buffer.getInt();
+      int offset = buffer.getInt();
+      int length = buffer.getInt();
+      return new RequestMessage(buffer, piece,
+              offset, length).validate(torrent);
+    }
+
+    public static RequestMessage craft(int piece, int offset, int length) {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + RequestMessage.BASE_SIZE);
+      buffer.putInt(RequestMessage.BASE_SIZE);
+      buffer.put(PeerMessage.Type.REQUEST.getTypeByte());
+      buffer.putInt(piece);
+      buffer.putInt(offset);
+      buffer.putInt(length);
+      return new RequestMessage(buffer, piece, offset, length);
+    }
+
+    public String toString() {
+      return super.toString() + " #" + this.getPiece() +
+              " (" + this.getLength() + "@" + this.getOffset() + ")";
+    }
+  }
+
+  /**
+   * Piece message.
+   *
+   * <len=0009+X><id=7><piece index><block offset><block data>
+   */
+  public static class PieceMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 9;
+
+    private int piece;
+    private int offset;
+    private ByteBuffer block;
+
+    private PieceMessage(ByteBuffer buffer, int piece,
+                         int offset, ByteBuffer block) {
+      super(Type.PIECE, buffer);
+      this.piece = piece;
+      this.offset = offset;
+      this.block = block;
+    }
+
+    public int getPiece() {
+      return this.piece;
+    }
+
+    public int getOffset() {
+      return this.offset;
+    }
+
+    public ByteBuffer getBlock() {
+      return this.block;
+    }
+
+    @Override
+    public PieceMessage validate(TorrentInfo torrent)
+            throws MessageValidationException {
+      if (this.piece >= 0 && this.piece < torrent.getPieceCount() &&
+              this.offset + this.block.limit() <=
+                      torrent.getPieceSize(this.piece)) {
+        return this;
+      }
+
+      throw new MessageValidationException(this);
+    }
+
+    public static PieceMessage parse(ByteBuffer buffer,
+                                     TorrentInfo torrent) throws MessageValidationException {
+      int piece = buffer.getInt();
+      int offset = buffer.getInt();
+      ByteBuffer block = buffer.slice();
+      return new PieceMessage(buffer, piece, offset, block)
+              .validate(torrent);
+    }
+
+    public static PieceMessage craft(int piece, int offset,
+                                     ByteBuffer buffer) {
+      return new PieceMessage(buffer, piece, offset, Constants.EMPTY_BUFFER);
+    }
+
+    public static ByteBuffer createBufferWithHeaderForMessage(int piece, int offset, int blockSize) {
+      ByteBuffer result = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + PieceMessage.BASE_SIZE + blockSize);
+      result.putInt(PieceMessage.BASE_SIZE + blockSize);
+      result.put(PeerMessage.Type.PIECE.getTypeByte());
+      result.putInt(piece);
+      result.putInt(offset);
+      return result;
+    }
+
+    public String toString() {
+      return super.toString() + " #" + this.getPiece() +
+              " (" + this.getBlock().capacity() + "@" + this.getOffset() + ")";
+    }
+  }
+
+  /**
+   * Cancel message.
+   *
+   * <len=00013><id=8><piece index><block offset><block length>
+   */
+  public static class CancelMessage extends PeerMessage {
+
+    private static final int BASE_SIZE = 13;
+
+    private int piece;
+    private int offset;
+    private int length;
+
+    private CancelMessage(ByteBuffer buffer, int piece,
+                          int offset, int length) {
+      super(Type.CANCEL, buffer);
+      this.piece = piece;
+      this.offset = offset;
+      this.length = length;
+    }
+
+    public int getPiece() {
+      return this.piece;
+    }
+
+    public int getOffset() {
+      return this.offset;
+    }
+
+    public int getLength() {
+      return this.length;
+    }
+
+    @Override
+    public CancelMessage validate(TorrentInfo torrent)
+            throws MessageValidationException {
+      if (this.piece >= 0 && this.piece < torrent.getPieceCount() &&
+              this.offset + this.length <=
+                      torrent.getPieceSize(this.piece)) {
+        return this;
+      }
+
+      throw new MessageValidationException(this);
+    }
+
+    public static CancelMessage parse(ByteBuffer buffer,
+                                      TorrentInfo torrent) throws MessageValidationException {
+      int piece = buffer.getInt();
+      int offset = buffer.getInt();
+      int length = buffer.getInt();
+      return new CancelMessage(buffer, piece,
+              offset, length).validate(torrent);
+    }
+
+    public static CancelMessage craft(int piece, int offset, int length) {
+      ByteBuffer buffer = ByteBuffer.allocate(
+              MESSAGE_LENGTH_FIELD_SIZE + CancelMessage.BASE_SIZE);
+      buffer.putInt(CancelMessage.BASE_SIZE);
+      buffer.put(PeerMessage.Type.CANCEL.getTypeByte());
+      buffer.putInt(piece);
+      buffer.putInt(offset);
+      buffer.putInt(length);
+      return new CancelMessage(buffer, piece, offset, length);
+    }
+
+    public String toString() {
+      return super.toString() + " #" + this.getPiece() +
+              " (" + this.getLength() + "@" + this.getOffset() + ")";
+    }
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java
new file mode 100644
index 0000000..bd25837
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/TrackerMessage.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol;
+
+import com.turn.ttorrent.common.Peer;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+
+/**
+ * BitTorrent tracker protocol messages representations.
+ *
+ * <p>
+ * This class and its <em>*TrackerMessage</em> subclasses provide POJO
+ * representations of the tracker protocol messages, for at least HTTP and UDP
+ * trackers' protocols, along with easy parsing from an input ByteBuffer to
+ * quickly get a usable representation of an incoming message.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public abstract class TrackerMessage {
+
+  /**
+   * Message type.
+   */
+  public enum Type {
+    UNKNOWN(-1),
+    CONNECT_REQUEST(0),
+    CONNECT_RESPONSE(0),
+    ANNOUNCE_REQUEST(1),
+    ANNOUNCE_RESPONSE(1),
+    SCRAPE_REQUEST(2),
+    SCRAPE_RESPONSE(2),
+    ERROR(3);
+
+    private final int id;
+
+    Type(int id) {
+      this.id = id;
+    }
+
+    public int getId() {
+      return this.id;
+    }
+  }
+
+  private final Type type;
+  private final ByteBuffer data;
+
+  /**
+   * Constructor for the base tracker message type.
+   *
+   * @param type The message type.
+   * @param data A byte buffer containing the binary data of the message (a
+   *             B-encoded map, a UDP packet data, etc.).
+   */
+  protected TrackerMessage(Type type, ByteBuffer data) {
+    this.type = type;
+    this.data = data;
+    if (this.data != null) {
+      this.data.rewind();
+    }
+  }
+
+  /**
+   * Returns the type of this tracker message.
+   */
+  public Type getType() {
+    return this.type;
+  }
+
+  /**
+   * Returns the encoded binary data for this message.
+   */
+  public ByteBuffer getData() {
+    return this.data;
+  }
+
+  /**
+   * Generic exception for message format and message validation exceptions.
+   */
+  public static class MessageValidationException extends Exception {
+
+    static final long serialVersionUID = -1;
+
+    public MessageValidationException(String s) {
+      super(s);
+    }
+
+    public MessageValidationException(String s, Throwable cause) {
+      super(s, cause);
+    }
+
+  }
+
+
+  /**
+   * Base interface for connection request messages.
+   *
+   * <p>
+   * This interface must be implemented by all subtypes of connection request
+   * messages for the various tracker protocols.
+   * </p>
+   *
+   * @author mpetazzoni
+   */
+  public interface ConnectionRequestMessage {
+
+  }
+
+
+  /**
+   * Base interface for connection response messages.
+   *
+   * <p>
+   * This interface must be implemented by all subtypes of connection
+   * response messages for the various tracker protocols.
+   * </p>
+   *
+   * @author mpetazzoni
+   */
+  public interface ConnectionResponseMessage {
+
+  }
+
+
+  /**
+   * Base interface for tracker error messages.
+   *
+   * <p>
+   * This interface must be implemented by all subtypes of tracker error
+   * messages for the various tracker protocols.
+   * </p>
+   *
+   * @author mpetazzoni
+   */
+  public interface ErrorMessage {
+
+    /**
+     * The various tracker error states.
+     *
+     * <p>
+     * These errors are reported by the tracker to a client when expected
+     * parameters or conditions are not present while processing an
+     * announce request from a BitTorrent client.
+     * </p>
+     */
+    enum FailureReason {
+      UNKNOWN_TORRENT("The requested torrent does not exist on this tracker"),
+      MISSING_HASH("Missing info hash"),
+      MISSING_PEER_ID("Missing peer ID"),
+      MISSING_PORT("Missing port"),
+      INVALID_EVENT("Unexpected event for peer state"),
+      NOT_IMPLEMENTED("Feature not implemented");
+
+      private String message;
+
+      FailureReason(String message) {
+        this.message = message;
+      }
+
+      public String getMessage() {
+        return this.message;
+      }
+    }
+
+    String getReason();
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java
new file mode 100644
index 0000000..8e25185
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceRequestMessage.java
@@ -0,0 +1,313 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.http;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentUtils;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * The announce request message for the HTTP tracker protocol.
+ * announce请求包含的信息
+ * <p>
+ * This class represents the announce request message in the HTTP tracker
+ * protocol. It doesn't add any specific fields compared to the generic
+ * announce request message, but it provides the means to parse such
+ * messages and craft them.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public class HTTPAnnounceRequestMessage extends HTTPTrackerMessage
+        implements AnnounceRequestMessage {
+
+  private final byte[] infoHash;
+  private final Peer peer;
+  private final long uploaded;
+  private final long downloaded;
+  private final long left;
+  private final boolean compact;
+  private final boolean noPeerId;
+  private final RequestEvent event;
+  private final int numWant;
+
+  private HTTPAnnounceRequestMessage(ByteBuffer data,
+                                     byte[] infoHash, Peer peer, long uploaded, long downloaded,
+                                     long left, boolean compact, boolean noPeerId, RequestEvent event,
+                                     int numWant) {
+    super(Type.ANNOUNCE_REQUEST, data);
+    this.infoHash = infoHash;
+    this.peer = peer;
+    this.downloaded = downloaded;
+    this.uploaded = uploaded;
+    this.left = left;
+    this.compact = compact;
+    this.noPeerId = noPeerId;
+    this.event = event;
+    this.numWant = numWant;
+  }
+
+  @Override
+  public byte[] getInfoHash() {
+    return this.infoHash;
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return TorrentUtils.byteArrayToHexString(this.infoHash);
+  }
+
+  @Override
+  public byte[] getPeerId() {
+    return this.peer.getPeerIdArray();
+  }
+
+  @Override
+  public String getHexPeerId() {
+    return this.peer.getHexPeerId();
+  }
+
+  @Override
+  public int getPort() {
+    return this.peer.getPort();
+  }
+
+  @Override
+  public long getUploaded() {
+    return this.uploaded;
+  }
+
+  @Override
+  public long getDownloaded() {
+    return this.downloaded;
+  }
+
+  @Override
+  public long getLeft() {
+    return this.left;
+  }
+
+  @Override
+  public boolean isCompact() {
+    return this.compact;
+  }
+
+  @Override
+  public boolean canOmitPeerId() {
+    return this.noPeerId;
+  }
+
+  @Override
+  public RequestEvent getEvent() {
+    return this.event;
+  }
+
+  @Override
+  public String getIp() {
+    return this.peer.getIp();
+  }
+
+  @Override
+  public int getNumWant() {
+    return this.numWant;
+  }
+
+  @Override
+  public String getKey() {
+    return "";
+  }
+
+  @Override
+  public String getTrackerId() {
+    return "";
+  }
+
+  /**
+   * Build the announce request URL for the given tracker announce URL.
+   *
+   * @param trackerAnnounceURL The tracker's announce URL.
+   * @return The URL object representing the announce request URL.
+   */
+   // 构造 announce请求的url
+  public URL buildAnnounceURL(URL trackerAnnounceURL)
+          throws UnsupportedEncodingException, MalformedURLException {
+    String base = trackerAnnounceURL.toString();
+    StringBuilder url = new StringBuilder(base);
+    url.append(base.contains("?") ? "&" : "?")
+            .append("info_hash=")
+            .append(URLEncoder.encode(
+                    new String(this.getInfoHash(), Constants.BYTE_ENCODING),
+                    Constants.BYTE_ENCODING))
+            .append("&peer_id=")
+            .append(URLEncoder.encode(
+                    new String(this.getPeerId(), Constants.BYTE_ENCODING),
+                    Constants.BYTE_ENCODING))
+            .append("&port=").append(this.getPort())
+            .append("&uploaded=").append(this.getUploaded())
+            .append("&downloaded=").append(this.getDownloaded())
+            .append("&left=").append(this.getLeft())
+            .append("&compact=").append(this.isCompact() ? 1 : 0)
+            .append("&no_peer_id=").append(this.canOmitPeerId() ? 1 : 0);
+
+    if (this.getEvent() != null &&
+            !RequestEvent.NONE.equals(this.getEvent())) {
+      url.append("&event=").append(this.getEvent().getEventName());
+    }
+
+    if (this.getIp() != null) {
+      url.append("&ip=").append(this.getIp());
+    }
+
+    return new URL(url.toString());
+  }
+
+  // 解析(parse)tracker请求
+  // 将Tracker收到的B编码请求数据解析为结构化消息对象。
+  public static HTTPAnnounceRequestMessage parse(BEValue decoded)
+          throws IOException, MessageValidationException {
+    if (decoded == null) {
+      throw new MessageValidationException(
+              "Could not decode tracker message (not B-encoded?)!");
+    }
+
+    Map<String, BEValue> params = decoded.getMap();
+
+    if (!params.containsKey("info_hash")) {
+      throw new MessageValidationException(
+              ErrorMessage.FailureReason.MISSING_HASH.getMessage());
+    }
+
+    if (!params.containsKey("peer_id")) {
+      throw new MessageValidationException(
+              ErrorMessage.FailureReason.MISSING_PEER_ID.getMessage());
+    }
+
+    if (!params.containsKey("port")) {
+      throw new MessageValidationException(
+              ErrorMessage.FailureReason.MISSING_PORT.getMessage());
+    }
+
+    try {
+      byte[] infoHash = params.get("info_hash").getBytes();
+      byte[] peerId = params.get("peer_id").getBytes();
+      int port = params.get("port").getInt();
+
+      // Default 'uploaded' and 'downloaded' to 0 if the client does
+      // not provide it (although it should, according to the spec).
+      long uploaded = 0;
+      if (params.containsKey("uploaded")) {
+        uploaded = params.get("uploaded").getLong();
+      }
+
+      long downloaded = 0;
+      if (params.containsKey("downloaded")) {
+        downloaded = params.get("downloaded").getLong();
+      }
+
+      // Default 'left' to -1 to avoid peers entering the COMPLETED
+      // state when they don't provide the 'left' parameter.
+      long left = -1;
+      if (params.containsKey("left")) {
+        left = params.get("left").getLong();
+      }
+
+      boolean compact = false;
+      if (params.containsKey("compact")) {
+        compact = params.get("compact").getInt() == 1;
+      }
+
+      boolean noPeerId = false;
+      if (params.containsKey("no_peer_id")) {
+        noPeerId = params.get("no_peer_id").getInt() == 1;
+      }
+
+      int numWant = AnnounceRequestMessage.DEFAULT_NUM_WANT;
+      if (params.containsKey("numwant")) {
+        numWant = params.get("numwant").getInt();
+      }
+
+      String ip = null;
+      if (params.containsKey("ip")) {
+        ip = params.get("ip").getString(Constants.BYTE_ENCODING);
+      }
+
+      RequestEvent event = RequestEvent.NONE;
+      if (params.containsKey("event")) {
+        event = RequestEvent.getByName(params.get("event")
+                .getString(Constants.BYTE_ENCODING));
+      }
+
+      return new HTTPAnnounceRequestMessage(Constants.EMPTY_BUFFER, infoHash,
+              new Peer(ip, port, ByteBuffer.wrap(peerId)),
+              uploaded, downloaded, left, compact, noPeerId,
+              event, numWant);
+    } catch (InvalidBEncodingException ibee) {
+      throw new MessageValidationException(
+              "Invalid HTTP tracker request!", ibee);
+    }
+  }
+
+  // 创建信息
+  //将Java类型参数转换为B编码字典
+  public static HTTPAnnounceRequestMessage craft(byte[] infoHash,
+                                                 byte[] peerId, int port, long uploaded, long downloaded, long left,
+                                                 boolean compact, boolean noPeerId, RequestEvent event,
+                                                 String ip, int numWant)
+          throws IOException {
+    Map<String, BEValue> params = new HashMap<String, BEValue>();
+    params.put("info_hash", new BEValue(infoHash));
+    params.put("peer_id", new BEValue(peerId));
+    params.put("port", new BEValue(port));
+    params.put("uploaded", new BEValue(uploaded));
+    params.put("downloaded", new BEValue(downloaded));
+    params.put("left", new BEValue(left));
+    params.put("compact", new BEValue(compact ? 1 : 0));
+    params.put("no_peer_id", new BEValue(noPeerId ? 1 : 0));
+
+    if (event != null) {
+      params.put("event",
+              new BEValue(event.getEventName(), Constants.BYTE_ENCODING));
+    }
+
+    if (ip != null) {
+      params.put("ip",
+              new BEValue(ip, Constants.BYTE_ENCODING));
+    }
+
+    if (numWant != AnnounceRequestMessage.DEFAULT_NUM_WANT) {
+      params.put("numwant", new BEValue(numWant));
+    }
+
+    return new HTTPAnnounceRequestMessage(
+            BEncoder.bencode(params),
+            infoHash, new Peer(ip, port, ByteBuffer.wrap(peerId)),
+            uploaded, downloaded, left, compact, noPeerId, event, numWant);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java
new file mode 100644
index 0000000..e8b5efc
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessage.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.http;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.protocol.AnnounceResponseMessage;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.*;
+
+
+/**
+ * The announce response message from an HTTP tracker.
+ *
+ * @author mpetazzoni
+ */
+public class HTTPAnnounceResponseMessage extends HTTPTrackerMessage
+        implements AnnounceResponseMessage {
+
+  private final int interval;
+  private final int complete;
+  private final int incomplete;
+  private final List<Peer> peers;
+  private String hexInfoHash;
+
+  private HTTPAnnounceResponseMessage(ByteBuffer data,
+                                      int interval, int complete, int incomplete, List<Peer> peers) {
+    super(Type.ANNOUNCE_RESPONSE, data);
+    this.interval = interval;
+    this.complete = complete;
+    this.incomplete = incomplete;
+    this.peers = peers;
+  }
+
+  private HTTPAnnounceResponseMessage(ByteBuffer data,
+                                      int interval, int complete, int incomplete, List<Peer> peers, String hexInfoHash) {
+    this(data, interval, complete, incomplete, peers);
+    this.hexInfoHash = hexInfoHash;
+  }
+
+  @Override
+  public int getInterval() {
+    return this.interval;
+  }
+
+  @Override
+  public int getComplete() {
+    return this.complete;
+  }
+
+  @Override
+  public int getIncomplete() {
+    return this.incomplete;
+  }
+
+  @Override
+  public List<Peer> getPeers() {
+    return this.peers;
+  }
+
+  public String getHexInfoHash() {
+    return this.hexInfoHash;
+  }
+
+  public static HTTPAnnounceResponseMessage parse(BEValue decoded)
+          throws IOException, MessageValidationException {
+    if (decoded == null) {
+      throw new MessageValidationException(
+              "Could not decode tracker message (not B-encoded?)!");
+    }
+
+    Map<String, BEValue> params = decoded.getMap();
+
+    if (params.get("interval") == null) {
+      throw new MessageValidationException("Tracker message missing mandatory field 'interval'!");
+    }
+
+    try {
+      List<Peer> peers;
+
+      try {
+        // First attempt to decode a compact response, since we asked
+        // for it.
+        peers = toPeerList(params.get("peers").getBytes());
+      } catch (InvalidBEncodingException ibee) {
+        // Fall back to peer list, non-compact response, in case the
+        // tracker did not support compact responses.
+        peers = toPeerList(params.get("peers").getList());
+      }
+
+      if (params.get("torrentIdentifier") != null) {
+        return new HTTPAnnounceResponseMessage(Constants.EMPTY_BUFFER,
+                params.get("interval").getInt(),
+                params.get("complete") != null ? params.get("complete").getInt() : 0,
+                params.get("incomplete") != null ? params.get("incomplete").getInt() : 0,
+                peers, params.get("torrentIdentifier").getString());
+      } else {
+        return new HTTPAnnounceResponseMessage(Constants.EMPTY_BUFFER,
+                params.get("interval").getInt(),
+                params.get("complete") != null ? params.get("complete").getInt() : 0,
+                params.get("incomplete") != null ? params.get("incomplete").getInt() : 0,
+                peers);
+      }
+    } catch (InvalidBEncodingException ibee) {
+      throw new MessageValidationException("Invalid response " +
+              "from tracker!", ibee);
+    } catch (UnknownHostException uhe) {
+      throw new MessageValidationException("Invalid peer " +
+              "in tracker response!", uhe);
+    }
+  }
+
+  /**
+   * Build a peer list as a list of {@link Peer}s from the
+   * announce response's peer list (in non-compact mode).
+   *
+   * @param peers The list of {@link BEValue}s dictionaries describing the
+   *              peers from the announce response.
+   * @return A {@link List} of {@link Peer}s representing the
+   * peers' addresses. Peer IDs are lost, but they are not crucial.
+   */
+  private static List<Peer> toPeerList(List<BEValue> peers)
+          throws InvalidBEncodingException {
+    List<Peer> result = new LinkedList<Peer>();
+
+    for (BEValue peer : peers) {
+      Map<String, BEValue> peerInfo = peer.getMap();
+      result.add(new Peer(
+              peerInfo.get("ip").getString(Constants.BYTE_ENCODING),
+              peerInfo.get("port").getInt()));
+    }
+
+    return result;
+  }
+
+  /**
+   * Build a peer list as a list of {@link Peer}s from the
+   * announce response's binary compact peer list.
+   *
+   * @param data The bytes representing the compact peer list from the
+   *             announce response.
+   * @return A {@link List} of {@link Peer}s representing the
+   * peers' addresses. Peer IDs are lost, but they are not crucial.
+   */
+  private static List<Peer> toPeerList(byte[] data)
+          throws InvalidBEncodingException, UnknownHostException {
+    if (data.length % 6 != 0) {
+      throw new InvalidBEncodingException("Invalid peers " +
+              "binary information string!");
+    }
+
+    List<Peer> result = new LinkedList<Peer>();
+    ByteBuffer peers = ByteBuffer.wrap(data);
+
+    for (int i = 0; i < data.length / 6; i++) {
+      byte[] ipBytes = new byte[4];
+      peers.get(ipBytes);
+      InetAddress ip = InetAddress.getByAddress(ipBytes);
+      int port =
+              (0xFF & (int) peers.get()) << 8 |
+                      (0xFF & (int) peers.get());
+      result.add(new Peer(new InetSocketAddress(ip, port)));
+    }
+
+    return result;
+  }
+
+  /**
+   * Craft a compact announce response message with a torrent identifier.
+   *
+   * @param interval
+   * @param complete
+   * @param incomplete
+   * @param peers
+   */
+  public static HTTPAnnounceResponseMessage craft(int interval,
+                                                  int complete, int incomplete,
+                                                  List<Peer> peers, String hexInfoHash) throws IOException, UnsupportedEncodingException {
+    Map<String, BEValue> response = new HashMap<String, BEValue>();
+    response.put("interval", new BEValue(interval));
+    response.put("complete", new BEValue(complete));
+    response.put("incomplete", new BEValue(incomplete));
+    if (hexInfoHash != null) {
+      response.put("torrentIdentifier", new BEValue(hexInfoHash));
+    }
+
+    ByteBuffer data = ByteBuffer.allocate(peers.size() * 6);
+    for (Peer peer : peers) {
+      byte[] ip = peer.getRawIp();
+      if (ip == null || ip.length != 4) {
+        continue;
+      }
+      data.put(ip);
+      data.putShort((short) peer.getPort());
+    }
+    response.put("peers", new BEValue(Arrays.copyOf(data.array(), data.position())));
+
+    return new HTTPAnnounceResponseMessage(
+            BEncoder.bencode(response),
+            interval, complete, incomplete, peers, hexInfoHash);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java
new file mode 100644
index 0000000..bb3d1a7
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerErrorMessage.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.http;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import com.turn.ttorrent.common.protocol.TrackerMessage.ErrorMessage;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * An error message from an HTTP tracker.
+ *
+ * @author mpetazzoni
+ */
+public class HTTPTrackerErrorMessage extends HTTPTrackerMessage
+        implements ErrorMessage {
+
+  private final String reason;
+
+  private HTTPTrackerErrorMessage(ByteBuffer data, String reason) {
+    super(Type.ERROR, data);
+    this.reason = reason;
+  }
+
+  @Override
+  public String getReason() {
+    return this.reason;
+  }
+
+  public static HTTPTrackerErrorMessage parse(BEValue decoded)
+          throws IOException, MessageValidationException {
+    if (decoded == null) {
+      throw new MessageValidationException(
+              "Could not decode tracker message (not B-encoded?)!");
+    }
+
+    Map<String, BEValue> params = decoded.getMap();
+
+    try {
+      return new HTTPTrackerErrorMessage(
+              Constants.EMPTY_BUFFER,
+              params.get("failure reason")
+                      .getString(Constants.BYTE_ENCODING));
+    } catch (InvalidBEncodingException ibee) {
+      throw new MessageValidationException("Invalid tracker error " +
+              "message!", ibee);
+    }
+  }
+
+  public static HTTPTrackerErrorMessage craft(
+          ErrorMessage.FailureReason reason) throws IOException {
+    return HTTPTrackerErrorMessage.craft(reason.getMessage());
+  }
+
+  public static HTTPTrackerErrorMessage craft(String reason)
+          throws IOException {
+    Map<String, BEValue> params = new HashMap<String, BEValue>();
+    params.put("failure reason",
+            new BEValue(reason, Constants.BYTE_ENCODING));
+    return new HTTPTrackerErrorMessage(
+            BEncoder.bencode(params),
+            reason);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java
new file mode 100644
index 0000000..239c2bc
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/http/HTTPTrackerMessage.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.http;
+
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+
+/**
+ * Base class for HTTP tracker messages.
+ *
+ * @author mpetazzoni
+ */
+public abstract class HTTPTrackerMessage extends TrackerMessage {
+
+  protected HTTPTrackerMessage(Type type, ByteBuffer data) {
+    super(type, data);
+  }
+
+  public static HTTPTrackerMessage parse(InputStream data)
+          throws IOException, MessageValidationException {
+    BEValue decoded = BDecoder.bdecode(data);
+    if (decoded == null) {
+      throw new MessageValidationException("Could not decode tracker message (not B-encoded?)!: ");
+    }
+    return parse(decoded);
+  }
+
+  public static HTTPTrackerMessage parse(BEValue decoded) throws IOException, MessageValidationException {
+    Map<String, BEValue> params = decoded.getMap();
+
+    if (params.containsKey("info_hash")) {
+      return HTTPAnnounceRequestMessage.parse(decoded);
+    } else if (params.containsKey("peers")) {
+      return HTTPAnnounceResponseMessage.parse(decoded);
+    } else if (params.containsKey("failure reason")) {
+      return HTTPTrackerErrorMessage.parse(decoded);
+    }
+
+    throw new MessageValidationException("Unknown HTTP tracker message!");
+  }
+
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java
new file mode 100644
index 0000000..7c53b02
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceRequestMessage.java
@@ -0,0 +1,259 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.common.TorrentUtils;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+
+
+/**
+ * The announce request message for the UDP tracker protocol.
+ *
+ * @author mpetazzoni
+ */
+public class UDPAnnounceRequestMessage
+        extends UDPTrackerMessage.UDPTrackerRequestMessage
+        implements AnnounceRequestMessage {
+
+  private static final int UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE = 98;
+
+  private final long connectionId;
+  private final int actionId = Type.ANNOUNCE_REQUEST.getId();
+  private final int transactionId;
+  private final byte[] infoHash;
+  private final byte[] peerId;
+  private final long downloaded;
+  private final long uploaded;
+  private final long left;
+  private final RequestEvent event;
+  private final InetAddress ip;
+  private final int numWant;
+  private final int key;
+  private final short port;
+
+  private UDPAnnounceRequestMessage(ByteBuffer data, long connectionId,
+                                    int transactionId, byte[] infoHash, byte[] peerId, long downloaded,
+                                    long uploaded, long left, RequestEvent event, InetAddress ip,
+                                    int key, int numWant, short port) {
+    super(Type.ANNOUNCE_REQUEST, data);
+    this.connectionId = connectionId;
+    this.transactionId = transactionId;
+    this.infoHash = infoHash;
+    this.peerId = peerId;
+    this.downloaded = downloaded;
+    this.uploaded = uploaded;
+    this.left = left;
+    this.event = event;
+    this.ip = ip;
+    this.key = key;
+    this.numWant = numWant;
+    this.port = port;
+  }
+
+  public long getConnectionId() {
+    return this.connectionId;
+  }
+
+  @Override
+  public int getActionId() {
+    return this.actionId;
+  }
+
+  @Override
+  public int getTransactionId() {
+    return this.transactionId;
+  }
+
+  @Override
+  public byte[] getInfoHash() {
+    return this.infoHash;
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return TorrentUtils.byteArrayToHexString(this.infoHash);
+  }
+
+  @Override
+  public byte[] getPeerId() {
+    return this.peerId;
+  }
+
+  @Override
+  public String getHexPeerId() {
+    return TorrentUtils.byteArrayToHexString(this.peerId);
+  }
+
+  @Override
+  public int getPort() {
+    return this.port;
+  }
+
+  @Override
+  public long getUploaded() {
+    return this.uploaded;
+  }
+
+  @Override
+  public long getDownloaded() {
+    return this.downloaded;
+  }
+
+  @Override
+  public long getLeft() {
+    return this.left;
+  }
+
+  @Override
+  public boolean isCompact() {
+    return true;
+  }
+
+  @Override
+  public boolean canOmitPeerId() {
+    return true;
+  }
+
+  @Override
+  public RequestEvent getEvent() {
+    return this.event;
+  }
+
+  @Override
+  public String getIp() {
+    return this.ip.toString();
+  }
+
+  @Override
+  public int getNumWant() {
+    return this.numWant;
+  }
+
+  @Override
+  public String getKey() {
+    return "";
+  }
+
+  @Override
+  public String getTrackerId() {
+    return "";
+  }
+
+  public static UDPAnnounceRequestMessage parse(ByteBuffer data)
+          throws MessageValidationException {
+    if (data.remaining() != UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE) {
+      throw new MessageValidationException(
+              "Invalid announce request message size!");
+    }
+
+    long connectionId = data.getLong();
+
+    if (data.getInt() != Type.ANNOUNCE_REQUEST.getId()) {
+      throw new MessageValidationException(
+              "Invalid action code for announce request!");
+    }
+
+    int transactionId = data.getInt();
+    byte[] infoHash = new byte[20];
+    data.get(infoHash);
+    byte[] peerId = new byte[20];
+    data.get(peerId);
+    long downloaded = data.getLong();
+    long uploaded = data.getLong();
+    long left = data.getLong();
+
+    RequestEvent event = RequestEvent.getById(data.getInt());
+    if (event == null) {
+      throw new MessageValidationException(
+              "Invalid event type in announce request!");
+    }
+
+    InetAddress ip = null;
+    try {
+      byte[] ipBytes = new byte[4];
+      data.get(ipBytes);
+      ip = InetAddress.getByAddress(ipBytes);
+    } catch (UnknownHostException uhe) {
+      throw new MessageValidationException(
+              "Invalid IP address in announce request!");
+    }
+
+    int key = data.getInt();
+    int numWant = data.getInt();
+    short port = data.getShort();
+
+    return new UDPAnnounceRequestMessage(data,
+            connectionId,
+            transactionId,
+            infoHash,
+            peerId,
+            downloaded,
+            uploaded,
+            left,
+            event,
+            ip,
+            key,
+            numWant,
+            port);
+  }
+
+  public static UDPAnnounceRequestMessage craft(long connectionId,
+                                                int transactionId, byte[] infoHash, byte[] peerId, long downloaded,
+                                                long uploaded, long left, RequestEvent event, InetAddress ip,
+                                                int key, int numWant, int port) {
+    if (infoHash.length != 20 || peerId.length != 20) {
+      throw new IllegalArgumentException();
+    }
+
+    if (!(ip instanceof Inet4Address)) {
+      throw new IllegalArgumentException("Only IPv4 addresses are " +
+              "supported by the UDP tracer protocol!");
+    }
+
+    ByteBuffer data = ByteBuffer.allocate(UDP_ANNOUNCE_REQUEST_MESSAGE_SIZE);
+    data.putLong(connectionId);
+    data.putInt(Type.ANNOUNCE_REQUEST.getId());
+    data.putInt(transactionId);
+    data.put(infoHash);
+    data.put(peerId);
+    data.putLong(downloaded);
+    data.putLong(left);
+    data.putLong(uploaded);
+    data.putInt(event.getId());
+    data.put(ip.getAddress());
+    data.putInt(key);
+    data.putInt(numWant);
+    data.putShort((short) port);
+    return new UDPAnnounceRequestMessage(data,
+            connectionId,
+            transactionId,
+            infoHash,
+            peerId,
+            downloaded,
+            uploaded,
+            left,
+            event,
+            ip,
+            key,
+            numWant,
+            (short) port);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java
new file mode 100644
index 0000000..0f5c34b
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessage.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.protocol.AnnounceResponseMessage;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * The announce response message for the UDP tracker protocol.
+ *
+ * @author mpetazzoni
+ */
+public class UDPAnnounceResponseMessage
+        extends UDPTrackerMessage.UDPTrackerResponseMessage
+        implements AnnounceResponseMessage {
+
+  private static final int UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE = 20;
+
+  private final int actionId = Type.ANNOUNCE_RESPONSE.getId();
+  private final int transactionId;
+  private final int interval;
+  private final int complete;
+  private final int incomplete;
+  private final List<Peer> peers;
+
+  private UDPAnnounceResponseMessage(ByteBuffer data, int transactionId,
+                                     int interval, int complete, int incomplete, List<Peer> peers) {
+    super(Type.ANNOUNCE_REQUEST, data);
+    this.transactionId = transactionId;
+    this.interval = interval;
+    this.complete = complete;
+    this.incomplete = incomplete;
+    this.peers = peers;
+  }
+
+  @Override
+  public int getActionId() {
+    return this.actionId;
+  }
+
+  @Override
+  public int getTransactionId() {
+    return this.transactionId;
+  }
+
+  @Override
+  public int getInterval() {
+    return this.interval;
+  }
+
+  @Override
+  public int getComplete() {
+    return this.complete;
+  }
+
+  @Override
+  public int getIncomplete() {
+    return this.incomplete;
+  }
+
+  @Override
+  public List<Peer> getPeers() {
+    return this.peers;
+  }
+
+  public String getHexInfoHash() {
+    return "";
+  }
+
+  public static UDPAnnounceResponseMessage parse(ByteBuffer data)
+          throws MessageValidationException {
+    if (data.remaining() < UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE ||
+            (data.remaining() - UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE) % 6 != 0) {
+      throw new MessageValidationException(
+              "Invalid announce response message size!");
+    }
+
+    if (data.getInt() != Type.ANNOUNCE_RESPONSE.getId()) {
+      throw new MessageValidationException(
+              "Invalid action code for announce response!");
+    }
+
+    int transactionId = data.getInt();
+    int interval = data.getInt();
+    int incomplete = data.getInt();
+    int complete = data.getInt();
+
+    List<Peer> peers = new LinkedList<Peer>();
+    int totalPeersInResponse = data.remaining() / 6;
+    for (int i = 0; i < totalPeersInResponse; i++) {
+      try {
+        byte[] ipBytes = new byte[4];
+        data.get(ipBytes);
+        InetAddress ip = InetAddress.getByAddress(ipBytes);
+        int port =
+                (0xFF & (int) data.get()) << 8 |
+                        (0xFF & (int) data.get());
+        peers.add(new Peer(new InetSocketAddress(ip, port)));
+      } catch (UnknownHostException uhe) {
+        throw new MessageValidationException(
+                "Invalid IP address in announce request!");
+      }
+    }
+
+    return new UDPAnnounceResponseMessage(data,
+            transactionId,
+            interval,
+            complete,
+            incomplete,
+            peers);
+  }
+
+  public static UDPAnnounceResponseMessage craft(int transactionId,
+                                                 int interval, int complete, int incomplete, List<Peer> peers) {
+    ByteBuffer data = ByteBuffer
+            .allocate(UDP_ANNOUNCE_RESPONSE_MIN_MESSAGE_SIZE + 6 * peers.size());
+    data.putInt(Type.ANNOUNCE_RESPONSE.getId());
+    data.putInt(transactionId);
+    data.putInt(interval);
+
+    /**
+     * Leechers (incomplete) are first, before seeders (complete) in the packet.
+     */
+    data.putInt(incomplete);
+    data.putInt(complete);
+
+    for (Peer peer : peers) {
+      byte[] ip = peer.getRawIp();
+      if (ip == null || ip.length != 4) {
+        continue;
+      }
+
+      data.put(ip);
+      data.putShort((short) peer.getPort());
+    }
+
+    return new UDPAnnounceResponseMessage(data,
+            transactionId,
+            interval,
+            complete,
+            incomplete,
+            peers);
+  }
+}
+
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java
new file mode 100644
index 0000000..3d9b981
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectRequestMessage.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * The connection request message for the UDP tracker protocol.
+ *
+ * @author mpetazzoni
+ */
+public class UDPConnectRequestMessage
+        extends UDPTrackerMessage.UDPTrackerRequestMessage
+        implements TrackerMessage.ConnectionRequestMessage {
+
+  private static final int UDP_CONNECT_REQUEST_MESSAGE_SIZE = 16;
+  private static final long UDP_CONNECT_REQUEST_MAGIC = 0x41727101980L;
+
+  private final long connectionId = UDP_CONNECT_REQUEST_MAGIC;
+  private final int actionId = Type.CONNECT_REQUEST.getId();
+  private final int transactionId;
+
+  private UDPConnectRequestMessage(ByteBuffer data, int transactionId) {
+    super(Type.CONNECT_REQUEST, data);
+    this.transactionId = transactionId;
+  }
+
+  public long getConnectionId() {
+    return this.connectionId;
+  }
+
+  @Override
+  public int getActionId() {
+    return this.actionId;
+  }
+
+  @Override
+  public int getTransactionId() {
+    return this.transactionId;
+  }
+
+  public static UDPConnectRequestMessage parse(ByteBuffer data)
+          throws MessageValidationException {
+    if (data.remaining() != UDP_CONNECT_REQUEST_MESSAGE_SIZE) {
+      throw new MessageValidationException(
+              "Invalid connect request message size!");
+    }
+
+    if (data.getLong() != UDP_CONNECT_REQUEST_MAGIC) {
+      throw new MessageValidationException(
+              "Invalid connection ID in connection request!");
+    }
+
+    if (data.getInt() != Type.CONNECT_REQUEST.getId()) {
+      throw new MessageValidationException(
+              "Invalid action code for connection request!");
+    }
+
+    return new UDPConnectRequestMessage(data,
+            data.getInt() // transactionId
+    );
+  }
+
+  public static UDPConnectRequestMessage craft(int transactionId) {
+    ByteBuffer data = ByteBuffer
+            .allocate(UDP_CONNECT_REQUEST_MESSAGE_SIZE);
+    data.putLong(UDP_CONNECT_REQUEST_MAGIC);
+    data.putInt(Type.CONNECT_REQUEST.getId());
+    data.putInt(transactionId);
+    return new UDPConnectRequestMessage(data,
+            transactionId);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java
new file mode 100644
index 0000000..396b3b7
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPConnectResponseMessage.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * The connection response message for the UDP tracker protocol.
+ *
+ * @author mpetazzoni
+ */
+public class UDPConnectResponseMessage
+        extends UDPTrackerMessage.UDPTrackerResponseMessage
+        implements TrackerMessage.ConnectionResponseMessage {
+
+  private static final int UDP_CONNECT_RESPONSE_MESSAGE_SIZE = 16;
+
+  private final int actionId = Type.CONNECT_RESPONSE.getId();
+  private final int transactionId;
+  private final long connectionId;
+
+  private UDPConnectResponseMessage(ByteBuffer data, int transactionId,
+                                    long connectionId) {
+    super(Type.CONNECT_RESPONSE, data);
+    this.transactionId = transactionId;
+    this.connectionId = connectionId;
+  }
+
+  @Override
+  public int getActionId() {
+    return this.actionId;
+  }
+
+  @Override
+  public int getTransactionId() {
+    return this.transactionId;
+  }
+
+  public long getConnectionId() {
+    return this.connectionId;
+  }
+
+  public static UDPConnectResponseMessage parse(ByteBuffer data)
+          throws MessageValidationException {
+    if (data.remaining() != UDP_CONNECT_RESPONSE_MESSAGE_SIZE) {
+      throw new MessageValidationException(
+              "Invalid connect response message size!");
+    }
+
+    if (data.getInt() != Type.CONNECT_RESPONSE.getId()) {
+      throw new MessageValidationException(
+              "Invalid action code for connection response!");
+    }
+
+    return new UDPConnectResponseMessage(data,
+            data.getInt(), // transactionId
+            data.getLong() // connectionId
+    );
+  }
+
+  public static UDPConnectResponseMessage craft(int transactionId,
+                                                long connectionId) {
+    ByteBuffer data = ByteBuffer
+            .allocate(UDP_CONNECT_RESPONSE_MESSAGE_SIZE);
+    data.putInt(Type.CONNECT_RESPONSE.getId());
+    data.putInt(transactionId);
+    data.putLong(connectionId);
+    return new UDPConnectResponseMessage(data,
+            transactionId,
+            connectionId);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java
new file mode 100644
index 0000000..4966177
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerErrorMessage.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+
+
+/**
+ * The error message for the UDP tracker protocol.
+ *
+ * @author mpetazzoni
+ */
+public class UDPTrackerErrorMessage
+        extends UDPTrackerMessage.UDPTrackerResponseMessage
+        implements TrackerMessage.ErrorMessage {
+
+  private static final int UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE = 8;
+
+  private final int actionId = Type.ERROR.getId();
+  private final int transactionId;
+  private final String reason;
+
+  private UDPTrackerErrorMessage(ByteBuffer data, int transactionId,
+                                 String reason) {
+    super(Type.ERROR, data);
+    this.transactionId = transactionId;
+    this.reason = reason;
+  }
+
+  @Override
+  public int getActionId() {
+    return this.actionId;
+  }
+
+  @Override
+  public int getTransactionId() {
+    return this.transactionId;
+  }
+
+  @Override
+  public String getReason() {
+    return this.reason;
+  }
+
+  public static UDPTrackerErrorMessage parse(ByteBuffer data)
+          throws MessageValidationException {
+    if (data.remaining() < UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE) {
+      throw new MessageValidationException(
+              "Invalid tracker error message size!");
+    }
+
+    if (data.getInt() != Type.ERROR.getId()) {
+      throw new MessageValidationException(
+              "Invalid action code for tracker error!");
+    }
+
+    int transactionId = data.getInt();
+    byte[] reasonBytes = new byte[data.remaining()];
+    data.get(reasonBytes);
+
+    try {
+      return new UDPTrackerErrorMessage(data,
+              transactionId,
+              new String(reasonBytes, Constants.BYTE_ENCODING)
+      );
+    } catch (UnsupportedEncodingException uee) {
+      throw new MessageValidationException(
+              "Could not decode error message!", uee);
+    }
+  }
+
+  public static UDPTrackerErrorMessage craft(int transactionId,
+                                             String reason) throws UnsupportedEncodingException {
+    byte[] reasonBytes = reason.getBytes(Constants.BYTE_ENCODING);
+    ByteBuffer data = ByteBuffer
+            .allocate(UDP_TRACKER_ERROR_MIN_MESSAGE_SIZE +
+                    reasonBytes.length);
+    data.putInt(Type.ERROR.getId());
+    data.putInt(transactionId);
+    data.put(reasonBytes);
+    return new UDPTrackerErrorMessage(data,
+            transactionId,
+            reason);
+  }
+}
diff --git a/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java
new file mode 100644
index 0000000..608014b
--- /dev/null
+++ b/ttorrent-master/common/src/main/java/com/turn/ttorrent/common/protocol/udp/UDPTrackerMessage.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright (C) 2012 Turn, Inc.
+ *
+ * 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.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base class for UDP tracker messages.
+ *
+ * @author mpetazzoni
+ */
+public abstract class UDPTrackerMessage extends TrackerMessage {
+
+  private UDPTrackerMessage(Type type, ByteBuffer data) {
+    super(type, data);
+  }
+
+  public abstract int getActionId();
+
+  public abstract int getTransactionId();
+
+  public static abstract class UDPTrackerRequestMessage
+          extends UDPTrackerMessage {
+
+    private static final int UDP_MIN_REQUEST_PACKET_SIZE = 16;
+
+    protected UDPTrackerRequestMessage(Type type, ByteBuffer data) {
+      super(type, data);
+    }
+
+    public static UDPTrackerRequestMessage parse(ByteBuffer data)
+            throws MessageValidationException {
+      if (data.remaining() < UDP_MIN_REQUEST_PACKET_SIZE) {
+        throw new MessageValidationException("Invalid packet size!");
+      }
+
+      /**
+       * UDP request packets always start with the connection ID (8 bytes),
+       * followed by the action (4 bytes). Extract the action code
+       * accordingly.
+       */
+      data.mark();
+      data.getLong();
+      int action = data.getInt();
+      data.reset();
+
+      if (action == Type.CONNECT_REQUEST.getId()) {
+        return UDPConnectRequestMessage.parse(data);
+      } else if (action == Type.ANNOUNCE_REQUEST.getId()) {
+        return UDPAnnounceRequestMessage.parse(data);
+      }
+
+      throw new MessageValidationException("Unknown UDP tracker " +
+              "request message!");
+    }
+  }
+
+  public static abstract class UDPTrackerResponseMessage
+          extends UDPTrackerMessage {
+
+    private static final int UDP_MIN_RESPONSE_PACKET_SIZE = 8;
+
+    protected UDPTrackerResponseMessage(Type type, ByteBuffer data) {
+      super(type, data);
+    }
+
+    public static UDPTrackerResponseMessage parse(ByteBuffer data)
+            throws MessageValidationException {
+      if (data.remaining() < UDP_MIN_RESPONSE_PACKET_SIZE) {
+        throw new MessageValidationException("Invalid packet size!");
+      }
+
+      /**
+       * UDP response packets always start with the action (4 bytes), so
+       * we can extract it immediately.
+       */
+      data.mark();
+      int action = data.getInt();
+      data.reset();
+
+      if (action == Type.CONNECT_RESPONSE.getId()) {
+        return UDPConnectResponseMessage.parse(data);
+      } else if (action == Type.ANNOUNCE_RESPONSE.getId()) {
+        return UDPAnnounceResponseMessage.parse(data);
+      } else if (action == Type.ERROR.getId()) {
+        return UDPTrackerErrorMessage.parse(data);
+      }
+
+      throw new MessageValidationException("Unknown UDP tracker " +
+              "response message!");
+    }
+  }
+
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentParserTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentParserTest.java
new file mode 100644
index 0000000..dee5969
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentParserTest.java
@@ -0,0 +1,97 @@
+package com.turn.ttorrent.common;
+
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.*;
+
+import static org.testng.Assert.*;
+
+@Test
+public class TorrentParserTest {
+
+  private TorrentParser myTorrentParser;
+
+  @BeforeMethod
+  public void setUp() {
+    myTorrentParser = new TorrentParser();
+  }
+
+  public void testParseNullAnnounce() throws IOException {
+    final Map<String, BEValue> metadataMap = new HashMap<String, BEValue>();
+    final HashMap<String, BEValue> infoTable = new HashMap<String, BEValue>();
+    infoTable.put(TorrentMetadataKeys.PIECES, new BEValue(new byte[20]));
+    infoTable.put(TorrentMetadataKeys.PIECE_LENGTH, new BEValue(512));
+
+    infoTable.put(TorrentMetadataKeys.FILE_LENGTH, new BEValue(10));
+    infoTable.put(TorrentMetadataKeys.NAME, new BEValue("file.txt"));
+
+    metadataMap.put(TorrentMetadataKeys.INFO_TABLE, new BEValue(infoTable));
+
+    TorrentMetadata metadata = new TorrentParser().parse(BEncoder.bencode(metadataMap).array());
+
+    assertNull(metadata.getAnnounce());
+  }
+
+  public void parseTest() throws IOException {
+    final Map<String, BEValue> metadata = new HashMap<String, BEValue>();
+    final HashMap<String, BEValue> infoTable = new HashMap<String, BEValue>();
+
+    metadata.put("announce", new BEValue("http://localhost/announce"));
+
+    infoTable.put("piece length", new BEValue(4));
+
+    infoTable.put("pieces", new BEValue(new byte[100]));
+    infoTable.put("name", new BEValue("test.file"));
+    infoTable.put("length", new BEValue(19));
+
+    metadata.put("info", new BEValue(infoTable));
+
+    final TorrentMetadata torrentMetadata = myTorrentParser.parse(BEncoder.bencode(metadata).array());
+
+    assertEquals(torrentMetadata.getPieceLength(), 4);
+    assertEquals(torrentMetadata.getAnnounce(), "http://localhost/announce");
+    assertEquals(torrentMetadata.getDirectoryName(), "test.file");
+    assertNull(torrentMetadata.getAnnounceList());
+
+    List<BEValue> announceList = new ArrayList<BEValue>();
+    announceList.add(new BEValue(Collections.singletonList(new BEValue("http://localhost/announce"))));
+    announceList.add(new BEValue(Collections.singletonList(new BEValue("http://second/announce"))));
+    metadata.put("announce-list", new BEValue(announceList));
+
+    final TorrentMetadata torrentMetadataWithAnnounceList = myTorrentParser.parse(BEncoder.bencode(metadata).array());
+
+    final List<List<String>> actualAnnounceList = torrentMetadataWithAnnounceList.getAnnounceList();
+    assertNotNull(actualAnnounceList);
+    assertEquals(actualAnnounceList.get(0).get(0), "http://localhost/announce");
+    assertEquals(actualAnnounceList.get(1).get(0), "http://second/announce");
+
+  }
+
+  public void badBEPFormatTest() {
+    try {
+      myTorrentParser.parse("abcd".getBytes());
+      fail("This method must throw invalid bencoding exception");
+    } catch (InvalidBEncodingException e) {
+      //it's okay
+    }
+  }
+
+  public void missingRequiredFieldTest() {
+    Map<String, BEValue> map = new HashMap<String, BEValue>();
+    map.put("info", new BEValue(new HashMap<String, BEValue>()));
+
+    try {
+      myTorrentParser.parse(BEncoder.bencode(map).array());
+      fail("This method must throw invalid bencoding exception");
+    } catch (InvalidBEncodingException e) {
+      //it's okay
+    } catch (IOException e) {
+      fail("", e);
+    }
+  }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentUtilsTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentUtilsTest.java
new file mode 100644
index 0000000..f43622d
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/TorrentUtilsTest.java
@@ -0,0 +1,68 @@
+/*
+  Copyright (C) 2016 Philipp Henkel
+  <p>
+  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
+  <p>
+  http://www.apache.org/licenses/LICENSE-2.0
+  <p>
+  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.turn.ttorrent.common;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+public class TorrentUtilsTest {
+
+  @Test(expectedExceptions = NullPointerException.class)
+  public void testBytesToHexWithNull() {
+    //noinspection ResultOfMethodCallIgnored,ConstantConditions
+    TorrentUtils.byteArrayToHexString(null);
+  }
+
+  @Test
+  public void testBytesToHexWithEmptyByteArray() {
+    assertEquals("", TorrentUtils.byteArrayToHexString(new byte[0]));
+  }
+
+  @Test
+  public void testBytesToHexWithSingleByte() {
+    assertEquals("BC", TorrentUtils.byteArrayToHexString(new byte[]{
+            (byte) 0xBC
+    }));
+  }
+
+  @Test
+  public void testBytesToHexWithZeroByte() {
+    assertEquals("00", TorrentUtils.byteArrayToHexString(new byte[1]));
+  }
+
+  @Test
+  public void testBytesToHexWithLeadingZero() {
+    assertEquals("0053FF", TorrentUtils.byteArrayToHexString(new byte[]{
+            (byte) 0x00, (byte) 0x53, (byte) 0xFF
+    }));
+  }
+
+  @Test
+  public void testBytesToHexTrailingZero() {
+    assertEquals("AA004500", TorrentUtils.byteArrayToHexString(new byte[]{
+            (byte) 0xAA, (byte) 0x00, (byte) 0x45, (byte) 0x00
+    }));
+  }
+
+  @Test
+  public void testBytesToHexAllSymbols() {
+    assertEquals("0123456789ABCDEF", TorrentUtils.byteArrayToHexString(new byte[]{
+            (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67,
+            (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF
+    }));
+  }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/HashesCalculatorsTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/HashesCalculatorsTest.java
new file mode 100644
index 0000000..ef6ca77
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/HashesCalculatorsTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import com.turn.ttorrent.common.TorrentUtils;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static java.util.Arrays.asList;
+import static org.testng.Assert.assertEquals;
+
+@Test
+public class HashesCalculatorsTest {
+
+  private List<? extends PiecesHashesCalculator> implementations;
+  private ExecutorService executor;
+
+  @BeforeMethod
+  public void setUp() {
+    executor = Executors.newFixedThreadPool(4);
+    implementations = Arrays.asList(
+            new SingleThreadHashesCalculator(),
+            new MultiThreadHashesCalculator(executor, 3),
+            new MultiThreadHashesCalculator(executor, 20),
+            new MultiThreadHashesCalculator(executor, 1)
+    );
+  }
+
+  @AfterMethod
+  public void tearDown() throws InterruptedException {
+    executor.shutdown();
+    executor.awaitTermination(10, TimeUnit.SECONDS);
+  }
+
+  public void testEmptySource() throws IOException {
+    List<byte[]> sourceBytes = new ArrayList<byte[]>();
+    sourceBytes.add(new byte[]{1, 2});
+    sourceBytes.add(new byte[]{});
+    sourceBytes.add(new byte[]{3, 4});
+
+    HashingResult expected = new HashingResult(Collections.singletonList(
+            TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3, 4})),
+            asList(2L, 0L, 2L)
+    );
+    verifyImplementationsResults(sourceBytes, 512, expected);
+  }
+
+  public void testStreamsAsPiece() throws IOException {
+    List<byte[]> sourceBytes = new ArrayList<byte[]>();
+    sourceBytes.add(new byte[]{1, 2, 3, 4});
+    sourceBytes.add(new byte[]{5, 6, 7, 8});
+
+    HashingResult expected = new HashingResult(asList(
+            TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3, 4}),
+            TorrentUtils.calculateSha1Hash(new byte[]{5, 6, 7, 8})),
+            asList(4L, 4L)
+    );
+    verifyImplementationsResults(sourceBytes, 4, expected);
+  }
+
+  public void testReadingNotFullyBuffer() throws IOException {
+    List<byte[]> sourceBytes = new ArrayList<byte[]>();
+    sourceBytes.add(new byte[]{1, 2, 3, 4, 5, 6, 7});
+    HashingResult expected = new HashingResult(asList(
+            TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3, 4, 5}),
+            TorrentUtils.calculateSha1Hash(new byte[]{6, 7})),
+            Collections.singletonList(7L)
+    );
+
+    final int maxToRead = 2;
+    List<HashingResult> hashingResults = new ArrayList<HashingResult>();
+    for (PiecesHashesCalculator implementation : implementations) {
+      List<DataSourceHolder> sources = new ArrayList<DataSourceHolder>();
+      for (byte[] sourceByte : sourceBytes) {
+        final InputStream is = new ByteArrayInputStream(sourceByte) {
+          @Override
+          public synchronized int read(byte[] b, int off, int len) {
+            if (len <= maxToRead) {
+              return super.read(b, off, len);
+            }
+            if (pos >= count) {
+              return -1;
+            }
+
+            int avail = count - pos;
+            if (len > avail) {
+              len = avail;
+            }
+            if (len <= 0) {
+              return 0;
+            }
+            System.arraycopy(buf, pos, b, off, maxToRead);
+            pos += maxToRead;
+            return maxToRead;
+          }
+        };
+        sources.add(new DataSourceHolder() {
+          @Override
+          public InputStream getStream() {
+            return is;
+          }
+
+          @Override
+          public void close() throws IOException {
+            is.close();
+          }
+        });
+      }
+      hashingResults.add(implementation.calculateHashes(sources, 5));
+    }
+    for (HashingResult actual : hashingResults) {
+      assertHashingResult(actual, expected);
+    }
+  }
+
+  public void testWithSmallSource() throws IOException {
+    List<byte[]> sourceBytes = new ArrayList<byte[]>();
+    sourceBytes.add(new byte[]{0, 1, 2, 3, 4, 5, 4});
+    sourceBytes.add(new byte[]{-1, -2});
+    sourceBytes.add(new byte[]{6, 7, 8, 9, 10});
+    sourceBytes.add(new byte[]{1, 2, 3, 4});
+
+    HashingResult expected = new HashingResult(asList(
+            TorrentUtils.calculateSha1Hash(new byte[]{0, 1, 2, 3, 4, 5}),
+            TorrentUtils.calculateSha1Hash(new byte[]{4, -1, -2, 6, 7, 8}),
+            TorrentUtils.calculateSha1Hash(new byte[]{9, 10, 1, 2, 3, 4})),
+            asList(7L, 2L, 5L, 4L)
+    );
+    verifyImplementationsResults(sourceBytes, 6, expected);
+  }
+
+  public void testOneLargeSource() throws IOException {
+
+    int size = 1024 * 1024 * 100;//100mb
+    byte[] sourceBytes = new byte[size];
+    List<byte[]> hashes = new ArrayList<byte[]>();
+    final int pieceSize = 128 * 1024;//128kb
+    for (int i = 0; i < sourceBytes.length; i++) {
+      sourceBytes[i] = (byte) (i * i);
+      if (i % pieceSize == 0 && i > 0) {
+        byte[] forHashing = Arrays.copyOfRange(sourceBytes, i - pieceSize, i);
+        hashes.add(TorrentUtils.calculateSha1Hash(forHashing));
+      }
+    }
+    hashes.add(TorrentUtils.calculateSha1Hash(
+            Arrays.copyOfRange(sourceBytes, hashes.size() * pieceSize, size)
+    ));
+
+    HashingResult expected = new HashingResult(hashes, Collections.singletonList((long) size));
+
+    verifyImplementationsResults(Collections.singletonList(sourceBytes), pieceSize, expected);
+  }
+
+  private void verifyImplementationsResults(List<byte[]> sourceBytes,
+                                            int pieceSize,
+                                            HashingResult expected) throws IOException {
+    List<HashingResult> hashingResults = new ArrayList<HashingResult>();
+    for (PiecesHashesCalculator implementation : implementations) {
+      List<DataSourceHolder> sources = new ArrayList<DataSourceHolder>();
+      for (byte[] sourceByte : sourceBytes) {
+        addSource(sourceByte, sources);
+      }
+      hashingResults.add(implementation.calculateHashes(sources, pieceSize));
+    }
+    for (HashingResult actual : hashingResults) {
+      assertHashingResult(actual, expected);
+    }
+  }
+
+  private void assertHashingResult(HashingResult actual, HashingResult expected) {
+
+    assertEquals(actual.getHashes().size(), expected.getHashes().size());
+    for (int i = 0; i < actual.getHashes().size(); i++) {
+      assertEquals(actual.getHashes().get(i), expected.getHashes().get(i));
+    }
+    assertEquals(actual.getSourceSizes(), expected.getSourceSizes());
+  }
+
+  private void addSource(byte[] bytes, List<DataSourceHolder> sources) {
+    final ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
+    sources.add(new DataSourceHolder() {
+      @Override
+      public InputStream getStream() {
+        return stream;
+      }
+
+      @Override
+      public void close() {
+      }
+    });
+  }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/MetadataBuilderTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/MetadataBuilderTest.java
new file mode 100644
index 0000000..193b25a
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/creation/MetadataBuilderTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2000-2018 JetBrains s.r.o.
+ *
+ * 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.turn.ttorrent.common.creation;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.common.TorrentUtils;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.*;
+
+import static com.turn.ttorrent.common.TorrentMetadataKeys.*;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+@Test
+public class MetadataBuilderTest {
+
+  public void testMultiFileModeWithOneFile() throws IOException {
+    Map<String, BEValue> map = new MetadataBuilder()
+            .setDirectoryName("root")
+            .addDataSource(new ByteArrayInputStream(new byte[]{1, 2}), "path/some_file", true)
+            .buildBEP().getMap();
+    Map<String, BEValue> info = map.get(INFO_TABLE).getMap();
+    assertEquals(info.get(NAME).getString(), "root");
+    List<BEValue> files = info.get(FILES).getList();
+    assertEquals(files.size(), 1);
+    Map<String, BEValue> file = files.get(0).getMap();
+    assertEquals(file.get(FILE_LENGTH).getInt(), 2);
+
+    StringBuilder path = new StringBuilder();
+    Iterator<BEValue> iterator = file.get(FILE_PATH).getList().iterator();
+    if (iterator.hasNext()) {
+      path = new StringBuilder(iterator.next().getString());
+    }
+    while (iterator.hasNext()) {
+      path.append("/").append(iterator.next().getString());
+    }
+    assertEquals(path.toString(), "path/some_file");
+  }
+
+  public void testBuildWithSpecifiedHashes() throws IOException {
+    byte[] expectedHash = TorrentUtils.calculateSha1Hash(new byte[]{1, 2, 3});
+    Map<String, BEValue> metadata = new MetadataBuilder()
+            .setPiecesHashesCalculator(new PiecesHashesCalculator() {
+              @Override
+              public HashingResult calculateHashes(List<DataSourceHolder> sources, int pieceSize) {
+                throw new RuntimeException("should not be invoked");
+              }
+            })
+            .setFilesInfo(
+                    Collections.singletonList(expectedHash),
+                    Collections.singletonList("file"),
+                    Collections.singletonList(42L))
+            .setPieceLength(512)
+            .setTracker("http://localhost:12346")
+            .buildBEP().getMap();
+
+    assertEquals(metadata.get(ANNOUNCE).getString(), "http://localhost:12346");
+    Map<String, BEValue> info = metadata.get(INFO_TABLE).getMap();
+    assertEquals(info.get(PIECES).getBytes(), expectedHash);
+    assertEquals(info.get(NAME).getString(), "file");
+    assertEquals(info.get(FILE_LENGTH).getLong(), 42);
+  }
+
+  public void testSingleFile() throws IOException {
+
+    byte[] data = {1, 2, 12, 4, 5};
+    Map<String, BEValue> metadata = new MetadataBuilder()
+            .addDataSource(new ByteArrayInputStream(data), "singleFile.txt", true)
+            .setTracker("http://localhost:12346")
+            .buildBEP().getMap();
+    assertEquals(metadata.get(ANNOUNCE).getString(), "http://localhost:12346");
+    assertNull(metadata.get(CREATION_DATE_SEC));
+    assertNull(metadata.get(COMMENT));
+    assertEquals(metadata.get(CREATED_BY).getString(), "ttorrent library");
+
+    Map<String, BEValue> info = metadata.get(INFO_TABLE).getMap();
+    assertEquals(info.get(PIECES).getBytes().length / Constants.PIECE_HASH_SIZE, 1);
+    assertEquals(info.get(PIECE_LENGTH).getInt(), 512 * 1024);
+
+    assertEquals(info.get(FILE_LENGTH).getInt(), data.length);
+    assertEquals(info.get(NAME).getString(), "singleFile.txt");
+
+  }
+
+  public void testMultiFileWithOneFileValues() throws IOException {
+
+    byte[] data = {34, 2, 12, 4, 5};
+    List<String> paths = Arrays.asList("unix/path", "win\\path");
+    Map<String, BEValue> metadata = new MetadataBuilder()
+            .addDataSource(new ByteArrayInputStream(data), paths.get(0), true)
+            .addDataSource(new ByteArrayInputStream(data), paths.get(1), true)
+            .setDirectoryName("downloadDirName")
+            .buildBEP().getMap();
+
+    Map<String, BEValue> info = metadata.get(INFO_TABLE).getMap();
+    assertEquals(info.get(PIECES).getBytes().length, Constants.PIECE_HASH_SIZE);
+    assertEquals(info.get(PIECE_LENGTH).getInt(), 512 * 1024);
+    assertEquals(info.get(NAME).getString(), "downloadDirName");
+
+    int idx = 0;
+    for (BEValue value : info.get(FILES).getList()) {
+      Map<String, BEValue> fileInfo = value.getMap();
+      String path = paths.get(idx);
+      idx++;
+      String[] split = path.split("[/\\\\]");
+      List<BEValue> list = fileInfo.get(FILE_PATH).getList();
+
+      assertEquals(fileInfo.get(FILE_LENGTH).getInt(), data.length);
+      assertEquals(list.size(), split.length);
+
+      for (int i = 0; i < list.size(); i++) {
+        assertEquals(list.get(i).getString(), split[i]);
+      }
+    }
+
+  }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessageTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessageTest.java
new file mode 100644
index 0000000..42e2a13
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/http/HTTPAnnounceResponseMessageTest.java
@@ -0,0 +1,49 @@
+package com.turn.ttorrent.common.protocol.http;
+
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.testng.Assert.assertEquals;
+
+public class HTTPAnnounceResponseMessageTest {
+
+  @Test
+  public void parseTest() throws IOException, TrackerMessage.MessageValidationException {
+
+    Map<String, BEValue> trackerResponse = new HashMap<String, BEValue>();
+    trackerResponse.put("interval", new BEValue(5));
+    trackerResponse.put("complete", new BEValue(1));
+    trackerResponse.put("incomplete", new BEValue(0));
+
+    String ip = "192.168.1.1";
+    int port = 6881;
+    InetSocketAddress peerAddress = new InetSocketAddress(ip, port);
+    ByteBuffer binaryPeerAddress = ByteBuffer.allocate(6);
+    binaryPeerAddress.put(peerAddress.getAddress().getAddress());
+    binaryPeerAddress.putShort((short) port);
+    trackerResponse.put("peers", new BEValue(binaryPeerAddress.array()));
+
+    HTTPAnnounceResponseMessage parsedResponse = (HTTPAnnounceResponseMessage) HTTPAnnounceResponseMessage.parse(
+            new ByteArrayInputStream(BEncoder.bencode(trackerResponse).array()));
+
+    assertEquals(parsedResponse.getInterval(), 5);
+    assertEquals(parsedResponse.getComplete(), 1);
+    assertEquals(parsedResponse.getIncomplete(), 0);
+    List<Peer> peers = parsedResponse.getPeers();
+    assertEquals(peers.size(), 1);
+    Peer peer = peers.get(0);
+    assertEquals(peer.getIp(), ip);
+    assertEquals(peer.getPort(), port);
+  }
+}
diff --git a/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessageTest.java b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessageTest.java
new file mode 100644
index 0000000..b19f81c
--- /dev/null
+++ b/ttorrent-master/common/src/test/java/com/turn/ttorrent/common/protocol/udp/UDPAnnounceResponseMessageTest.java
@@ -0,0 +1,55 @@
+package com.turn.ttorrent.common.protocol.udp;
+
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import org.testng.annotations.Test;
+
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Specification of UDP announce protocol:
+ * <a href="http://www.bittorrent.org/beps/bep_0015.html">http://www.bittorrent.org/beps/bep_0015.html</a>
+ */
+public class UDPAnnounceResponseMessageTest {
+
+  @Test
+  public void parseTest() throws TrackerMessage.MessageValidationException {
+
+    final int peersCount = 3;
+    ByteBuffer response = ByteBuffer.allocate(20 + peersCount * 6);
+    response.putInt(1);//announce response message identifier
+    response.putInt(3);//transaction_id
+    response.putInt(5);//interval
+    response.putInt(1);//incomplete
+    response.putInt(2);//complete
+
+
+    String ipPrefix = "192.168.1.1";
+    final int firstPort = 6881;
+    for (int i = 0; i < peersCount; i++) {
+      String ip = ipPrefix + i;
+      InetSocketAddress peerAddress = new InetSocketAddress(ip, firstPort);
+      response.put(peerAddress.getAddress().getAddress());
+      response.putShort((short) (firstPort + i));
+    }
+
+    response.rewind();
+    UDPAnnounceResponseMessage parsedResponse = UDPAnnounceResponseMessage.parse(response);
+    assertEquals(parsedResponse.getActionId(), 1);
+    assertEquals(parsedResponse.getTransactionId(), 3);
+    assertEquals(parsedResponse.getInterval(), 5);
+    assertEquals(parsedResponse.getComplete(), 2);
+    assertEquals(parsedResponse.getIncomplete(), 1);
+    List<Peer> peers = parsedResponse.getPeers();
+    assertEquals(peers.size(), peersCount);
+    for (int i = 0; i < peersCount; i++) {
+      Peer peer = peers.get(i);
+      assertEquals(peer.getIp(), ipPrefix + i);
+      assertEquals(peer.getPort(), firstPort + i);
+    }
+  }
+}