tracker

Change-Id: I8f8ac81f9c4d7c7650cd64d2dade701dc6c11dce
diff --git a/pom.xml b/pom.xml
index a1921d6..7b65b05 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,7 @@
             <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
             <version>3.5.10.1</version>
         </dependency>
+
         <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-jsqlparser</artifactId>
@@ -125,6 +126,24 @@
             <version>2.9.0</version>
         </dependency>
 
+        <!--tracker 配置-->
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-tracker</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-client</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-bencoding</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+
+
     </dependencies>
 
     <build>
diff --git a/src/main/java/com/ptp/ptplatform/config/WebConfig.java b/src/main/java/com/ptp/ptplatform/config/WebConfig.java
index d0fdb8e..bb81631 100644
--- a/src/main/java/com/ptp/ptplatform/config/WebConfig.java
+++ b/src/main/java/com/ptp/ptplatform/config/WebConfig.java
@@ -18,4 +18,4 @@
                 .allowCredentials(true);  // 是否允许携带凭证
 
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/ptp/ptplatform/controller/InviteCodeController.java b/src/main/java/com/ptp/ptplatform/controller/InviteCodeController.java
index cf482c6..4a4e625 100644
--- a/src/main/java/com/ptp/ptplatform/controller/InviteCodeController.java
+++ b/src/main/java/com/ptp/ptplatform/controller/InviteCodeController.java
@@ -1,4 +1,58 @@
 package com.ptp.ptplatform.controller;
 
+
+import com.ptp.ptplatform.entity.INVITE_CODE;
+import com.ptp.ptplatform.entity.USER;
+import com.ptp.ptplatform.mapper.InviteCodeMapper;
+import com.ptp.ptplatform.mapper.UserMapper;
+import com.ptp.ptplatform.utils.JwtUtils;
+import com.ptp.ptplatform.utils.Result;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/invitecode")
 public class InviteCodeController {
+    //mapper和controller
+    @Resource
+    private InviteCodeMapper inviteCodeMapper;
+    @Autowired
+    private UserMapper userMapper;
+    @Autowired
+    private UserController userController;
+
+    //使用魔力值兑换邀请码
+    @PostMapping("/generate")
+    public Result generateInviteCode(HttpServletRequest request) {
+        USER user = userController.getUserInRequest(request);
+        System.out.println(user.getMagicPoints());
+        if (user.getMagicPoints() >= 10) {
+            user.generateInviteCode();
+            INVITE_CODE inviteCode = new INVITE_CODE(user.getUsername());
+
+            inviteCodeMapper.insertInviteCode(inviteCode);
+            userMapper.updateUser(user);
+
+            return Result.ok().data("inviteCode", inviteCode);
+
+        } else {
+            return Result.error(404).setMessage("兑换邀请码失败,魔力值不足。");
+        }
+
+    }
+
+    //用户获取持有的邀请码
+    @GetMapping("/userInviteCode")
+    public Result userInviteCode(HttpServletRequest request) {
+        USER user = userController.getUserInRequest(request);
+
+        List<INVITE_CODE> inviteCode = inviteCodeMapper.selectByUser(user.getUsername());
+        return Result.ok().data("inviteCode", inviteCode);
+    }
+
 }
diff --git a/src/main/java/com/ptp/ptplatform/controller/TorrentController.java b/src/main/java/com/ptp/ptplatform/controller/TorrentController.java
index 3c71ba1..436ecdd 100644
--- a/src/main/java/com/ptp/ptplatform/controller/TorrentController.java
+++ b/src/main/java/com/ptp/ptplatform/controller/TorrentController.java
@@ -1,121 +1,359 @@
-package com.ptp.ptplatform.controller;

-

-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

-import com.ptp.ptplatform.entity.TorrentComment;

-import com.ptp.ptplatform.entity.Torrent;

-import com.ptp.ptplatform.service.TorrentCommentService;

-import com.ptp.ptplatform.service.TorrentService;

-import com.ptp.ptplatform.utils.Result;

-import lombok.AllArgsConstructor;

-import org.springframework.web.bind.annotation.*;

-import com.baomidou.mybatisplus.core.metadata.IPage;

-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

-

-import java.time.LocalDateTime;

-import java.util.ArrayList;

-import java.util.HashMap;

-import java.util.List;

-import java.util.Map;

-

-@RestController

-@RequestMapping("/torrent")

-@AllArgsConstructor

-public class TorrentController {

-    private final TorrentService postService;

-    private final TorrentCommentService commentService;

-

-    // 创建帖子

-    @PostMapping

-    public Result createTorrent(@RequestBody Torrent torrent) {

-        postService.save(torrent);

-        return Result.ok().data("post", torrent);

-    }

-

-    // 列表分页

-    @GetMapping

-    public Result listPosts(@RequestParam(defaultValue = "1") int page,

-                            @RequestParam(defaultValue = "5") int size) {

-        IPage<Torrent> ipage = postService.page(

-                new Page<>(page, size),

-                new QueryWrapper<Torrent>().orderByDesc("create_time")

-        );

-        return Result.ok()

-                .data("records", ipage.getRecords())

-                .data("total", ipage.getTotal());

-    }

-

-    @GetMapping("/{Id}")

-    public Result getPost(@PathVariable int Id) {

-        Torrent torrent = postService.getById(Id);

-        if (torrent == null) {

-            return Result.error(404).setMessage("种子不存在"); // 明确设置404状态码

-        }

-

-        // 获取所有评论(按创建时间排序)

-        List<TorrentComment> allComments = commentService.list(

-                new QueryWrapper<TorrentComment>()

-                        .eq("post_id", Id)

-                        .orderByAsc("create_time")

-        );

-

-        // 构建评论树形结构

-        List<TorrentComment> rootComments = new ArrayList<>();

-        Map<Integer, TorrentComment> commentMap = new HashMap<>();

-

-        // 第一遍:初始化所有评论到map中

-        for (TorrentComment comment : allComments) {

-            comment.setReplies(new ArrayList<>()); // 初始化replies列表

-            commentMap.put(comment.getId(), comment);

-        }

-

-        // 第二遍:构建父子关系

-        for (TorrentComment comment : allComments) {

-            if (comment.getParentId() == 0) {

-                rootComments.add(comment);

-            } else {

-                TorrentComment parent = commentMap.get(comment.getParentId());

-                if (parent != null) {

-                    parent.getReplies().add(comment);

-                }

-            }

-        }

-

-        return Result.ok()

-                .data("torrent", torrent)

-                .data("comments", rootComments);

-    }

-

-    // 点赞帖子

-    @PostMapping("/{Id}/like")

-    public Result likePost(@PathVariable int Id) {

-        postService.incrementLike(Id);

-        return Result.ok();

-    }

-

-    @PostMapping("/{Id}/comments")

-    public Result comment(@PathVariable int Id,

-                          @RequestBody TorrentComment comment) {

-        // 设置评论信息

-        comment.setPostId(Id);

-        comment.setCreateTime(LocalDateTime.now());

-        comment.setLikeCount(0); // 初始化点赞数

-        comment.setParentId(0);  // 默认父评论ID

-        comment.setReplyTo(null); // 主评论 replyTo=null

-

-        // 保存评论

-        boolean saved = commentService.save(comment);

-        if (!saved) {

-            return Result.error(404).setMessage("评论保存失败");

-        }

-

-        // 更新回复数

-        postService.incrementReplyCount(Id);

-

-        // 获取更新后的完整评论(包含数据库生成的ID和时间)

-        TorrentComment newComment = commentService.getById(comment.getId());

-

-        return Result.ok()

-                .data("comment", newComment)  // 返回完整评论数据

-                .data("newReplyCount", postService.getById(Id).getReplyCount());

-    }

+package com.ptp.ptplatform.controller;
+
+import com.ptp.ptplatform.entity.TORRENT;
+import com.ptp.ptplatform.entity.USER;
+import com.ptp.ptplatform.mapper.TorrentMapper;
+import com.ptp.ptplatform.mapper.UserMapper;
+import com.ptp.ptplatform.service.ClientService;
+import com.ptp.ptplatform.service.TrackerService;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.ptp.ptplatform.entity.TorrentComment;
+import com.ptp.ptplatform.service.TorrentCommentService;
+import com.ptp.ptplatform.service.TorrentService;
+import com.ptp.ptplatform.utils.Result;
+import com.turn.ttorrent.common.TorrentStatistic;
+import com.turn.ttorrent.tracker.TrackedPeer;
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AllArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.Map;
+
+import static com.ptp.ptplatform.utils.JwtUtils.getClaimByToken;
+
+//实现种子的增删改查
+@RestController
+@RequestMapping("/torrent")
+@AllArgsConstructor
+public class TorrentController {
+    private final TorrentService postService;
+    private final TorrentCommentService commentService;
+
+    @Resource
+    private TorrentMapper torrentMapper;
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private UserController userController;
+
+    private TrackerService ts = new TrackerService();
+    private ClientService cs = new ClientService();
+
+    @GetMapping("/{Id}")
+    public Result getPost(@PathVariable int Id) {
+        TORRENT torrent = postService.getById(Id);
+        if (torrent == null) {
+            return Result.error(404).setMessage("种子不存在"); // 明确设置404状态码
+        }
+
+        // 获取所有评论(按创建时间排序)
+        List<TorrentComment> allComments = commentService.list(
+                new QueryWrapper<TorrentComment>()
+                        .eq("post_id", Id)
+                        .orderByAsc("create_time")
+        );
+
+        // 构建评论树形结构
+        List<TorrentComment> rootComments = new ArrayList<>();
+        Map<Integer, TorrentComment> commentMap = new HashMap<>();
+
+        // 第一遍:初始化所有评论到map中
+        for (TorrentComment comment : allComments) {
+            comment.setReplies(new ArrayList<>()); // 初始化replies列表
+            commentMap.put(comment.getId(), comment);
+        }
+
+        // 第二遍:构建父子关系
+        for (TorrentComment comment : allComments) {
+            if (comment.getParentId() == 0) {
+                rootComments.add(comment);
+            } else {
+                TorrentComment parent = commentMap.get(comment.getParentId());
+                if (parent != null) {
+                    parent.getReplies().add(comment);
+                }
+            }
+        }
+
+        return Result.ok()
+                .data("torrent", torrent)
+                .data("comments", rootComments);
+    }
+
+    // 点赞帖子
+    @PostMapping("/{Id}/like")
+    public Result likePost(@PathVariable int Id) {
+        postService.incrementLike(Id);
+        return Result.ok();
+    }
+
+    @PostMapping("/{Id}/comments")
+    public Result comment(@PathVariable int Id,
+                          @RequestBody TorrentComment comment) {
+        // 设置评论信息
+        comment.setPostId(Id);
+        comment.setCreateTime(LocalDateTime.now());
+        comment.setLikeCount(0); // 初始化点赞数
+        comment.setParentId(0);  // 默认父评论ID
+        comment.setReplyTo(null); // 主评论 replyTo=null
+
+        // 保存评论
+        boolean saved = commentService.save(comment);
+        if (!saved) {
+            return Result.error(404).setMessage("评论保存失败");
+        }
+
+        // 更新回复数
+        postService.incrementReplyCount(Id);
+
+        // 获取更新后的完整评论(包含数据库生成的ID和时间)
+        TorrentComment newComment = commentService.getById(comment.getId());
+
+        return Result.ok()
+                .data("comment", newComment)  // 返回完整评论数据
+                .data("newReplyCount", postService.getById(Id).getReplay_count());
+    }
+
+    @PostConstruct //启动项目时候自动启动tracker服务器
+    public String startTS() {
+        try {
+            ts.startTracker();
+            System.out.println("成功启动tracker服务器");
+            return "成功启动tracker服务器";
+        } catch (Exception e) {
+            System.out.println("启动失败: " + e.getMessage());
+            return "启动失败: " + e.getMessage();
+        }
+    }
+
+    //上传、下载、删除操作
+    //post 要添加数据库 文件和tracker的信息
+    @PostMapping
+    public Result addTorrent(HttpServletRequest request, @RequestBody TORRENT torrent) throws IOException {
+
+        // 1. 检查源文件
+        File file = new File(torrent.getFilePath());
+        if (!file.exists() || !file.isFile()) {
+            return Result.error(404).data("message", "文件不存在或路径无效");
+        }
+        if (!file.getName().endsWith(".torrent")) {
+            return Result.error(404).data("message", "仅支持.torrent文件");
+        }
+
+        // 2. 确定存储目录(改为项目根目录下的torrents文件夹)
+        String projectRoot = System.getProperty("user.dir");
+        String uploadDir = projectRoot + File.separator + "torrents";
+        File torrentsFolder = new File(uploadDir);
+        if (!torrentsFolder.exists() && !torrentsFolder.mkdirs()) {
+            return Result.error(404).data("message", "无法创建存储目录");
+        }
+
+        // 3. 复制文件
+        File uploadedFile = new File(torrentsFolder, file.getName());
+        try {
+            Files.copy(file.toPath(), uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+            System.out.println("文件保存成功: " + uploadedFile.getAbsolutePath());
+        } catch (IOException e) {
+            System.out.println("文件复制失败" + e);
+            return Result.error(404).data("message", "文件保存失败: " + e.getMessage());
+        }
+
+        // 4. 处理Tracker逻辑
+        try {
+            TrackedTorrent tt = ts.addTrackedTorrent(uploadedFile.getAbsolutePath());
+            TrackedPeer tp = createTrackedPeer(tt);
+            tt.addPeer(tp);
+
+            // 5. 数据库操作
+            torrent.setHash(tt.getHexInfoHash());
+            torrent.setSize(uploadedFile.length());
+            torrent.setUsername(getClaimByToken(request.getHeader("Authorization")).getSubject());
+            torrent.setFilePath(uploadedFile.getAbsolutePath());
+
+            if (torrentMapper.insertTorrent(torrent) == 1) {
+                return Result.ok().data("message", "上传成功")
+                        .data("path", uploadedFile.getAbsolutePath());
+            }
+        } catch (Exception e) {
+            System.out.println("Tracker或数据库操作失败" + e);
+            uploadedFile.delete(); // 回滚:删除已保存的文件
+        }
+
+        return Result.error(404).data("message", "上传失败");
+    }
+
+    //根据本机创建peer对象
+    public static TrackedPeer createTrackedPeer(TrackedTorrent torrent) {
+        try {
+            // 获取本机IP地址
+            InetAddress inetAddress = InetAddress.getLocalHost();
+            String ip = inetAddress.getHostAddress(); // 本机IP地址
+
+            // 假设使用一个随机的端口,这个端口可以根据需求调整
+            int port = 6881; // 这里使用固定端口,也可以通过动态方式分配
+
+            // 创建Peer ID (可以随机生成或从其他地方获取)
+            // 这里使用UUID来生成一个随机的 Peer ID
+            String peerIdString = UUID.randomUUID().toString();
+            ByteBuffer peerId = ByteBuffer.wrap(peerIdString.getBytes());
+
+            // 创建TrackedPeer实例
+            TrackedPeer trackedPeer = new TrackedPeer(torrent, ip, port, peerId);
+
+            return trackedPeer;
+        } catch (UnknownHostException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    //种子下载 根据id获取数据
+    // 能成功获取到数据
+    // 能够根据此值,完成对相关数据的修改
+    // getDownloadedBytes:3063963
+    //getUploadedBytes:0
+    @GetMapping("/downloadTorrent")
+    public Result getTorrent(HttpServletRequest request,int id ,String downloadPath) throws Exception {
+        TORRENT torrent = torrentMapper.selectByID(id);
+
+        // 下载完成
+        // 传入变量:下载种子的id 下载到文件的路径
+        cs.downloadTorrent(torrent.getFilePath(), downloadPath);
+
+        //管理种子相关数据
+        TorrentStatistic ts = cs.getStatistics(torrent.getFilePath());
+
+        //修改用户对应的信息
+        USER downloadUser = userController.getUserInRequest(request);
+        USER uploadUser = userMapper.selectByUsername(torrent.getUsername());
+
+        downloadUser.updateDownload(ts.getDownloadedBytes());
+        uploadUser.updateUpload(ts.getDownloadedBytes());
+
+        userMapper.updateUser(downloadUser);
+        userMapper.updateUser(uploadUser);
+
+
+        return Result.ok().data("torrent", torrent);
+    }
+
+    //根据id删除
+    @DeleteMapping("/deleteTorrent/{id}")
+    public Result deleteTorrent(HttpServletRequest request,@PathVariable("id")int id) throws Exception {
+        TORRENT torrent = torrentMapper.selectByID(id);
+
+        String filePath = torrent.getFilePath();
+        try {
+            // 检查文件是否存在
+            if (Files.exists(Paths.get(filePath))) {
+                Files.delete(Paths.get(filePath));  // 删除文件
+                torrentMapper.deleteTorrent(id);
+
+                return Result.ok().message("种子文件删除成功");
+            } else {
+                throw new IOException("File not found: " + filePath);
+            }
+        } catch (IOException e) {
+            // 异常处理
+            e.printStackTrace();
+            // 返回失败结果或其他处理逻辑
+            return Result.error(404).setMessage("种子文件删除失败");
+        }
+    }
+
+    // Controller
+    @GetMapping
+    public Result listPosts(@RequestParam(defaultValue = "1") int page,
+                            @RequestParam(defaultValue = "5") int size) {
+        int offset = (page - 1) * size;
+        List<TORRENT> pagedList = torrentMapper.selectAllTorrentWithPage(offset, size);
+        int total = torrentMapper.countAllTorrent();
+
+        Page<TORRENT> pageResult = new Page<>(page, size);
+        pageResult.setRecords(pagedList);
+        pageResult.setTotal(total);
+
+        return Result.ok()
+                .data("records", pageResult.getRecords())
+                .data("total", pageResult.getTotal());
+    }
+
+    // 获取特定分区下的种子(分页版)
+    @GetMapping("/get/torrentByCategory/{category}")
+    public Result getTorrentByCategory(
+            @PathVariable("category") String category,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "5") int size) {
+
+        int offset = (page - 1) * size;
+        List<TORRENT> pagedList = torrentMapper.selectTorrentByCategoryWithPage(category, offset, size);
+        int total = torrentMapper.countByCategory(category);
+
+        Page<TORRENT> pageResult = new Page<>(page, size);
+        pageResult.setRecords(pagedList);
+        pageResult.setTotal(total);
+
+        return Result.ok()
+                .data("records", pageResult.getRecords())
+                .data("total", pageResult.getTotal());
+    }
+
+    // 搜索种子关键字(分页版)
+    @GetMapping("/get/torrentByKey/{key}")
+    public Result getTorrentByKey(
+            @PathVariable("key") String key,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "5") int size) {
+
+        int offset = (page - 1) * size;
+        List<TORRENT> pagedList = torrentMapper.selectTorrentByKeyWithPage(key, offset, size);
+        int total = torrentMapper.countByKey(key);
+
+        Page<TORRENT> pageResult = new Page<>(page, size);
+        pageResult.setRecords(pagedList);
+        pageResult.setTotal(total);
+
+        return Result.ok()
+                .data("records", pageResult.getRecords())
+                .data("total", pageResult.getTotal());
+    }
+
+    // 获取对应用户种子(分页版)
+    @GetMapping("/get/torrentMyself")
+    public Result getTorrentByKey(
+            HttpServletRequest request,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "5") int size) throws Exception {
+
+        USER user = userController.getUserInRequest(request);
+        int offset = (page - 1) * size;
+        List<TORRENT> pagedList = torrentMapper.selectTorrentByUsernameWithPage(user.getUsername(), offset, size);
+        int total = torrentMapper.countByUsername(user.getUsername());
+
+        Page<TORRENT> pageResult = new Page<>(page, size);
+        pageResult.setRecords(pagedList);
+        pageResult.setTotal(total);
+
+        return Result.ok()
+                .data("records", pageResult.getRecords())
+                .data("total", pageResult.getTotal());
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/controller/UserController.java b/src/main/java/com/ptp/ptplatform/controller/UserController.java
index 9265ce7..4587abc 100644
--- a/src/main/java/com/ptp/ptplatform/controller/UserController.java
+++ b/src/main/java/com/ptp/ptplatform/controller/UserController.java
@@ -6,47 +6,58 @@
 import com.ptp.ptplatform.entity.*;
 import com.ptp.ptplatform.mapper.UserMapper;
 import com.ptp.ptplatform.mapper.InviteCodeMapper;
+import com.ptp.ptplatform.utils.SizeCalculation;
 import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import com.ptp.ptplatform.utils.Result;
 import com.ptp.ptplatform.utils.JwtUtils;
 import com.ptp.ptplatform.entity.USER;
+
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/user")
 @CrossOrigin //启用跨域
 public class UserController {
 
-    @Resource
+    @Autowired
     private UserMapper userMapper;
-    @Resource
+    @Autowired
     private InviteCodeMapper inviteCodeMapper;
 
+
+    //个人中心获取用户登录信息
     @GetMapping("/info") //获取
-    public Result info(String token) {
-        String username = JwtUtils.getClaimByToken(token).getSubject();
-        return Result.ok().data("name", username);
+    public Result info(HttpServletRequest request) {
+        USER user = this.getUserInRequest(request);
+        return Result.ok().data("info", user);
     }
 
     @PostMapping("/login") //用户登录
     public Result login(String username, String password) {
         USER user = userMapper.selectByUsername(username);
 
-        if (user != null) {
-            // 将用户输入的密码进行哈希处理
-            String hashedPassword = user.hashPassword(password);
-
-            // 比较用户输入的密码哈希值和数据库中的密码哈希值
-            if (hashedPassword.equals(user.getPassword())) {
-                String token = JwtUtils.generateToken(user.getUsername());
-                return Result.ok().data("token", token);  // 返回令牌给前端
-            } else {
-                return Result.error(404).setMessage("密码错误");
-            }
-        } else {
+        // 检查用户是否存在
+        if (user == null) {
             return Result.error(404).setMessage("用户不存在");
         }
+
+        System.out.println("password" + user.getPassword());
+
+        // 比较用户输入密码值是否正确
+        if (user.getPassword().equals(password)) {
+            String token = JwtUtils.generateToken(user.getUsername());
+
+            user.setLastLogin(new Date());
+            userMapper.updateUser(user);
+            return Result.ok().data("token", token);  // 返回令牌给前端
+        } else {
+            return Result.error(404).setMessage("密码错误");
+        }
     }
 
     @PostMapping("/regist")
@@ -55,12 +66,12 @@
         if (userCheck == null) {
             //获取邀请码
             INVITE_CODE inviteCode = inviteCodeMapper.selectByCode(code);
-            if(inviteCode != null){
+            if (inviteCode != null) {
                 System.out.println(inviteCode.getIsUsed());
 
-                if(!inviteCode.getIsUsed()){
+                if (!inviteCode.getIsUsed()) {
                     Date time = new Date();
-                    USER user = new USER(username, password, time) ;
+                    USER user = new USER(username, password, time);
 
                     userMapper.insertUser(user);
                     inviteCodeMapper.updateCodeUser(code);
@@ -81,9 +92,46 @@
 
     }
 
-    @PostMapping("/logout")
-    public Result logout(String token) {
-        return Result.ok();
+    //获取允许下载额度的相关信息
+    @GetMapping("/allowDownload")
+    public Result allowDownload(HttpServletRequest request) {
+        USER user = this.getUserInRequest(request);
+        // 总额度 已经使用 剩余 单位是GB
+        int totalSize = SizeCalculation.byteToGB(user.getUpload());
+        int usedSize = SizeCalculation.byteToGB(user.getDownload());
+        int leftSize = totalSize - usedSize;
+
+        // 将变量封装成Map
+        Map<String, Object> dataMap = new HashMap<>();
+        dataMap.put("totalSize", totalSize);
+        dataMap.put("usedSize", usedSize);
+        dataMap.put("leftSize", leftSize);
+
+        return Result.ok().data(dataMap);
+    }
+
+    //修改用户密码
+    @PutMapping("/password")
+    public Result updatePassword(HttpServletRequest request, @RequestBody Map<String, String> passwordMap) {
+        USER user = this.getUserInRequest(request);
+
+        String oldPassword = passwordMap.get("oldPassword");
+        String newPassword = passwordMap.get("newPassword");
+
+        if (user.getPassword().equals(oldPassword)) {
+            user.setPassword(newPassword);
+            userMapper.updateUser(user);
+
+            return Result.ok().setMessage("修改密码成功");
+        }
+        return Result.error(404).setMessage("原密码不正确");
+    }
+
+
+    //从http请求中获取到用户
+    public USER getUserInRequest(HttpServletRequest request) {
+        String UserName = JwtUtils.getClaimByToken(request.getHeader("Authorization")).getSubject();
+        return userMapper.selectByUsername(UserName);
     }
 
 }
diff --git a/src/main/java/com/ptp/ptplatform/entity/DOWNLOAD_RECORD.java b/src/main/java/com/ptp/ptplatform/entity/DOWNLOAD_RECORD.java
new file mode 100644
index 0000000..3743f4e
--- /dev/null
+++ b/src/main/java/com/ptp/ptplatform/entity/DOWNLOAD_RECORD.java
@@ -0,0 +1,23 @@
+package com.ptp.ptplatform.entity;
+
+import java.time.LocalDateTime;
+
+public class DOWNLOAD_RECORD {
+
+    private int id; // 下载记录ID
+    private int userId; // 下载用户ID
+    private int torrentId; // 请求的种子资源ID
+    private String status; // 下载状态(成功、失败、终止)
+    private LocalDateTime downloadTime; // 下载时间
+    private String downloadPath; // 下载路径
+
+    // 构造方法、getter和setter省略
+
+    public DOWNLOAD_RECORD(int userId, int torrentId, String status, LocalDateTime downloadTime, String downloadPath) {
+        this.userId = userId;
+        this.torrentId = torrentId;
+        this.status = status;
+        this.downloadTime = downloadTime;
+        this.downloadPath = downloadPath;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/entity/INVITE_CODE.java b/src/main/java/com/ptp/ptplatform/entity/INVITE_CODE.java
index d0a5a53..a35dd89 100644
--- a/src/main/java/com/ptp/ptplatform/entity/INVITE_CODE.java
+++ b/src/main/java/com/ptp/ptplatform/entity/INVITE_CODE.java
@@ -4,6 +4,7 @@
 
 import jakarta.persistence.*;
 import java.io.Serializable;
+import java.util.UUID;
 
 @Table(name = "invite_code")
 public class INVITE_CODE {
@@ -43,8 +44,10 @@
     // Constructor (optional)
     public INVITE_CODE() {}
 
-    public INVITE_CODE(String code, String generateUser) {
-        this.code = code;
+    //code是10位随机生成密码
+    public INVITE_CODE(String generateUser) {
+        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
+        this.code = uuid.substring(0, 8);
         this.generateUser = generateUser;
         this.isUsed = false;
     }
diff --git a/src/main/java/com/ptp/ptplatform/entity/TORRENT.java b/src/main/java/com/ptp/ptplatform/entity/TORRENT.java
new file mode 100644
index 0000000..afbb264
--- /dev/null
+++ b/src/main/java/com/ptp/ptplatform/entity/TORRENT.java
@@ -0,0 +1,62 @@
+package com.ptp.ptplatform.entity;

+

+import com.baomidou.mybatisplus.annotation.IdType;

+import com.baomidou.mybatisplus.annotation.TableField;

+import com.baomidou.mybatisplus.annotation.TableId;

+import com.baomidou.mybatisplus.annotation.TableName;

+import jakarta.persistence.GeneratedValue;

+import jakarta.persistence.GenerationType;

+import jakarta.persistence.Id;

+import lombok.Data;

+import java.time.LocalDateTime;

+import java.util.Date;

+

+@Data

+@TableName("torrent") // 使用MyBatis-Plus的注解指定表名

+public class TORRENT {

+

+    @Id

+    @GeneratedValue(strategy = GenerationType.IDENTITY)

+    private Integer id;

+

+    private String torrentName; // 文件名

+    private String description; // 描述

+    private String category; // 分类

+    private String region; // 地区

+    private String resolution; // 分辨率

+    private String subtitle; // 字幕状态

+

+    private Long size; // 种子对应文件大小

+    private String hash; // torrent编码

+    private String username; // 创建用户

+    private String filePath;

+    private int like_count;

+    private int replay_count;

+

+    @TableField(value = "create_time") // 使用@TableField注解处理时间字段

+    private Date createTime; // 创建时间

+

+    // 构造函数

+    public TORRENT(String hash, String torrentName, String description, String category, String region, String resolution, String subtitle, Long size, String username, String filePath) {

+        this.hash = hash;

+        this.torrentName = torrentName;

+        this.description = description;

+        this.category = category;

+        this.region = region;

+        this.resolution = resolution;

+        this.subtitle = subtitle;

+        this.size = size;

+        this.username = username;

+        this.filePath = filePath;

+        this.like_count = 0;

+        this.replay_count = 0;

+    }

+

+    public void setLikeCount(int like_count) {

+        this.like_count = like_count;

+    }

+

+    public int getLikeCount() {

+        return this.like_count;

+    }

+}
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/entity/Torrent.java b/src/main/java/com/ptp/ptplatform/entity/Torrent.java
deleted file mode 100644
index 2c6c6d3..0000000
--- a/src/main/java/com/ptp/ptplatform/entity/Torrent.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.ptp.ptplatform.entity;

-

-import com.baomidou.mybatisplus.annotation.IdType;

-import com.baomidou.mybatisplus.annotation.TableField;

-import com.baomidou.mybatisplus.annotation.TableId;

-import com.baomidou.mybatisplus.annotation.TableName;

-import lombok.Data;

-import java.time.LocalDateTime;

-

-@Data

-@TableName("torrent")

-public class Torrent {

-    @TableId(value = "id", type = IdType.AUTO)

-    private Integer id;

-

-    @TableField("torrentName") // 明确指定数据库列名

-    private String torrentName;

-

-    private String description;

-    private String category;

-    private String region;

-    private String resolution;

-    private String subtitle;

-    private Long size;

-    private String hash;

-

-    private LocalDateTime createTime;

-

-    private String username;

-

-    @TableField("filePath") // 明确指定数据库列名

-    private String filePath;

-

-    private Integer likeCount;

-    private Integer replyCount;

-

-

-

-}
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/entity/USER.java b/src/main/java/com/ptp/ptplatform/entity/USER.java
index a33847c..ac51eb4 100644
--- a/src/main/java/com/ptp/ptplatform/entity/USER.java
+++ b/src/main/java/com/ptp/ptplatform/entity/USER.java
@@ -27,10 +27,10 @@
     @Temporal(TemporalType.DATE)
     private Date lastLogin;
 
-    private int upload;
-    private int download;
-    private int shareRate;
-    private int magicPoints;
+    private long upload;
+    private long download;
+    private double shareRate;//分享率 前端展示数据应该为 90.23%这种
+    private long magicPoints;// 魔力值
 
     public enum Authority {
         USER, ADMIN, LIMIT, BAN
@@ -86,23 +86,23 @@
         this.lastLogin = lastLogin;
     }
 
-    public int getUpload() {
+    public long getUpload() {
         return upload;
     }
 
-    public void setUpload(int upload) {
+    public void setUpload(long upload) {
         this.upload = upload;
     }
 
-    public int getDownload() {
+    public long getDownload() {
         return download;
     }
 
-    public void setDownload(int download) {
+    public void setDownload(long download) {
         this.download = download;
     }
 
-    public int getShareRate() {
+    public double getShareRate() {
         return shareRate;
     }
 
@@ -110,8 +110,8 @@
         this.shareRate = shareRate;
     }
 
-    public int getMagicPoints() {
-        return magicPoints;
+    public long getMagicPoints() {
+        return this.magicPoints;
     }
 
     public void setMagicPoints(int magicPoints) {
@@ -125,32 +125,41 @@
         this.username = username;
         this.registTime = registTime;
 
-        this.password = hashPassword(password);;
+        this.password = password;
 
         this.authority = Authority.USER;
         this.level = 0;
         this.lastLogin = null;
         this.upload = 0;
         this.download = 0;
-        this.shareRate = 100;
+        this.shareRate = 1;
         this.magicPoints = 0;
     }
 
-    public String hashPassword(String password) {
-        try {
-            // 使用SHA-256算法
-            MessageDigest digest = MessageDigest.getInstance("SHA-256");
-            byte[] hashBytes = digest.digest(password.getBytes());
-
-            // 将字节数组转换为十六进制字符串
-            StringBuilder hexString = new StringBuilder();
-            for (byte b : hashBytes) {
-                hexString.append(String.format("%02x", b));
-            }
-
-            return hexString.toString();
-        } catch (NoSuchAlgorithmException e) {
-            throw new RuntimeException("Error hashing password", e);
-        }
+    //对上传量下载量的处理 返回更新后的数据结果
+    public long updateUpload(long addUpload) {
+        this.upload += addUpload;
+        this.shareRate = (double) this.upload / this.download;
+        return upload;
     }
+
+    public long updateDownload(long addDownload) {
+        this.download += addDownload;
+        this.shareRate = (double) this.upload / this.download;
+        return download;
+    }
+
+    //生成邀请码,魔力值扣除
+    public void generateInviteCode() {
+        this.magicPoints -= 10;
+    }
+
+    // 每天运行的计算魔力值程序
+    // 添加种子,验证同一用户的全部种子存活与否
+    public long updateMagicPoints(long addMagicPoints) {
+
+        return 0;
+    }
+
+
 }
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/entity/testdata.java b/src/main/java/com/ptp/ptplatform/entity/testdata.java
deleted file mode 100644
index 7d6d88e..0000000
--- a/src/main/java/com/ptp/ptplatform/entity/testdata.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// model类 负责管理数据库数据和sb的映射
-
-package com.ptp.ptplatform.entity;
-
-public class testdata {
-    private String name;
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-}
diff --git a/src/main/java/com/ptp/ptplatform/mapper/DownloadRecordMapper.java b/src/main/java/com/ptp/ptplatform/mapper/DownloadRecordMapper.java
new file mode 100644
index 0000000..4cb8c90
--- /dev/null
+++ b/src/main/java/com/ptp/ptplatform/mapper/DownloadRecordMapper.java
@@ -0,0 +1,10 @@
+package com.ptp.ptplatform.mapper;
+
+import com.ptp.ptplatform.entity.DOWNLOAD_RECORD;
+import org.apache.ibatis.annotations.Insert;
+
+public interface DownloadRecordMapper {
+    @Insert("INSERT INTO download_record (username, torrent_id, status, download_time, download_path) " +
+            "VALUES (#{username}, #{torrentId}, #{status}, #{downloadTime}, #{downloadPath})")
+    void insertDownloadRecord(DOWNLOAD_RECORD downloadRecord);
+}
diff --git a/src/main/java/com/ptp/ptplatform/mapper/InviteCodeMapper.java b/src/main/java/com/ptp/ptplatform/mapper/InviteCodeMapper.java
index 612c7fb..b58e01b 100644
--- a/src/main/java/com/ptp/ptplatform/mapper/InviteCodeMapper.java
+++ b/src/main/java/com/ptp/ptplatform/mapper/InviteCodeMapper.java
@@ -2,17 +2,27 @@
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ptp.ptplatform.entity.INVITE_CODE;
+import org.apache.ibatis.annotations.Insert;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
 import org.springframework.web.service.annotation.PutExchange;
 
+import java.util.List;
+
 @Mapper
 public interface InviteCodeMapper extends BaseMapper<INVITE_CODE> {
     //查询
     @Select("SELECT * FROM invite_code WHERE code = #{code}")
     INVITE_CODE selectByCode(String code);
 
+    @Select("SELECT * FROM invite_code WHERE generateUser = #{username}")
+    List<INVITE_CODE> selectByUser(String username);
+
     @Update("UPDATE invite_code SET isused = true WHERE code = #{code}")
     int updateCodeUser(String code);
+
+    // 插入新数据
+    @Insert("INSERT INTO invite_code (code, generateUser, isUsed) VALUES (#{code}, #{generateUser}, #{isUsed})")
+    int insertInviteCode(INVITE_CODE inviteCode);
 }
diff --git a/src/main/java/com/ptp/ptplatform/mapper/TorrentMapper.java b/src/main/java/com/ptp/ptplatform/mapper/TorrentMapper.java
index ff612b3..b4ec407 100644
--- a/src/main/java/com/ptp/ptplatform/mapper/TorrentMapper.java
+++ b/src/main/java/com/ptp/ptplatform/mapper/TorrentMapper.java
@@ -1,8 +1,76 @@
 package com.ptp.ptplatform.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.ptp.ptplatform.entity.Torrent;
-import org.apache.ibatis.annotations.Mapper;
+import com.ptp.ptplatform.entity.TORRENT;
+import org.apache.ibatis.annotations.*;
 
-@Mapper
-public interface TorrentMapper extends BaseMapper<Torrent> {}
\ No newline at end of file
+import java.util.List;
+
+public interface TorrentMapper extends BaseMapper<TORRENT> {
+    // 根据hash查询Torrent
+    @Select("SELECT * FROM torrent WHERE hash = #{hash}")
+    TORRENT selectByHash(String hash);
+
+    @Select("SELECT * FROM torrent WHERE id = #{id}")
+    TORRENT selectByID(int id);
+
+    // Mapper接口
+    @Select("SELECT * FROM torrent ORDER BY create_time DESC LIMIT #{offset}, #{size}")
+    List<TORRENT> selectAllTorrentWithPage(@Param("offset") int offset, @Param("size") int size);
+
+    @Select("SELECT COUNT(*) FROM torrent")
+    int countAllTorrent();
+
+    @Select("SELECT * FROM torrent WHERE category = #{category}")
+    List<TORRENT> selectTorrentByCategory(String category);
+
+    @Select("SELECT * FROM torrent WHERE torrentName LIKE CONCAT('%', #{key}, '%')")
+    List<TORRENT> selectTorrentByKey(String key);
+
+    @Select("SELECT * FROM torrent WHERE username = #{username}")
+    List<TORRENT> selectTorrentByUsername(String username);
+
+    //分页查询部分
+    // 在TorrentMapper.java中添加以下方法
+
+    // 分页查询特定分类
+    @Select("SELECT * FROM torrent WHERE category = #{category} ORDER BY create_time DESC LIMIT #{offset}, #{size}")
+    List<TORRENT> selectTorrentByCategoryWithPage(@Param("category") String category,
+                                                  @Param("offset") int offset,
+                                                  @Param("size") int size);
+
+    @Select("SELECT COUNT(*) FROM torrent WHERE category = #{category}")
+    int countByCategory(@Param("category") String category);
+
+    // 分页搜索关键字
+    @Select("SELECT * FROM torrent WHERE torrentName LIKE CONCAT('%',#{key},'%') OR description LIKE CONCAT('%',#{key},'%') ORDER BY create_time DESC LIMIT #{offset}, #{size}")
+    List<TORRENT> selectTorrentByKeyWithPage(@Param("key") String key,
+                                             @Param("offset") int offset,
+                                             @Param("size") int size);
+
+    @Select("SELECT COUNT(*) FROM torrent WHERE torrentName LIKE CONCAT('%',#{key},'%') OR description LIKE CONCAT('%',#{key},'%')")
+    int countByKey(@Param("key") String key);
+
+    // 分页查询用户种子
+    @Select("SELECT * FROM torrent WHERE username = #{username} ORDER BY create_time DESC LIMIT #{offset}, #{size}")
+    List<TORRENT> selectTorrentByUsernameWithPage(@Param("username") String username,
+                                                  @Param("offset") int offset,
+                                                  @Param("size") int size);
+
+    @Select("SELECT COUNT(*) FROM torrent WHERE username = #{username}")
+    int countByUsername(@Param("username") String username);
+
+    // 插入Torrent
+    @Insert("INSERT INTO torrent (torrentName, description, category, region, resolution, subtitle, size, hash, username, create_time, filePath) " +
+            "VALUES (#{torrentName}, #{description}, #{category}, #{region}, #{resolution}, #{subtitle}, #{size}, #{hash}, #{username}, CURRENT_TIMESTAMP, #{filePath})")
+    int insertTorrent(TORRENT torrent);
+
+    // 更新Torrent信息
+    @Update("UPDATE torrent SET torrentName = #{torrentName}, description = #{description}, category = #{category}, " +
+            "tags = #{tags}, size = #{size}, username = #{username} WHERE hash = #{hash}")
+    int updateTorrent(TORRENT torrent);
+
+    // 删除Torrent
+    @Delete("DELETE FROM torrent WHERE id = #{id}")
+    int deleteTorrent(int id);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/mapper/UserMapper.java b/src/main/java/com/ptp/ptplatform/mapper/UserMapper.java
index 024807e..f17d0dd 100644
--- a/src/main/java/com/ptp/ptplatform/mapper/UserMapper.java
+++ b/src/main/java/com/ptp/ptplatform/mapper/UserMapper.java
@@ -6,6 +6,7 @@
 import org.apache.ibatis.annotations.Insert;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 
 @Mapper
 public interface UserMapper extends BaseMapper<USER> {
@@ -20,4 +21,17 @@
     @Insert("INSERT INTO user (username, password, registTime) " +
             "VALUES (#{username}, #{password}, #{registTime})")
     int insertUser(USER user);
+
+    // 更新用户信息
+    @Update("UPDATE user SET " +
+            "password = #{password}, " +
+            "authority = #{authority}, " +
+            "level = #{level}, " +
+            "lastLogin = #{lastLogin}, " +
+            "upload = #{upload}, " +
+            "download = #{download}, " +
+            "shareRate = #{shareRate}, " +
+            "magicPoints = #{magicPoints} " +
+            "WHERE username = #{username}")
+    int updateUser(USER user);
 }
diff --git a/src/main/java/com/ptp/ptplatform/service/ClientService.java b/src/main/java/com/ptp/ptplatform/service/ClientService.java
new file mode 100644
index 0000000..39cdc76
--- /dev/null
+++ b/src/main/java/com/ptp/ptplatform/service/ClientService.java
@@ -0,0 +1,34 @@
+package com.ptp.ptplatform.service;
+
+import com.turn.ttorrent.client.SimpleClient;
+import com.turn.ttorrent.common.TorrentStatistic;
+import org.springframework.stereotype.Service;
+
+import java.net.InetAddress;
+
+@Service
+public class ClientService {
+    SimpleClient client = new SimpleClient();
+
+    public boolean downloadTorrent(String torrentFilePath, String outputDirectory) throws Exception {
+
+
+        InetAddress address = InetAddress.getLocalHost(); // 使用本地地址
+
+        try {
+            // 开始下载torrent
+            client.downloadTorrent(torrentFilePath, outputDirectory, address);
+            System.out.println("下载完成");
+            return true;
+        } catch (Exception e) {
+            // 下载失败,输出异常信息
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    public TorrentStatistic getStatistics(String torrentFilePath) throws Exception {
+        TorrentStatistic ts = client.getStatistics(torrentFilePath);
+        return ts;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/service/TorrentService.java b/src/main/java/com/ptp/ptplatform/service/TorrentService.java
index f3d0222..1891067 100644
--- a/src/main/java/com/ptp/ptplatform/service/TorrentService.java
+++ b/src/main/java/com/ptp/ptplatform/service/TorrentService.java
@@ -1,10 +1,10 @@
 package com.ptp.ptplatform.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
-import com.ptp.ptplatform.entity.Torrent;
+import com.ptp.ptplatform.entity.TORRENT;
 
-public interface TorrentService extends IService<Torrent> {
-    Torrent getTorrentById(Integer id);  // Add this method
+public interface TorrentService extends IService<TORRENT> {
+    TORRENT getTorrentById(Integer id);  // Add this method
     void incrementLike(int postId);
     void incrementReplyCount(int postId);
 
diff --git a/src/main/java/com/ptp/ptplatform/service/TrackerService.java b/src/main/java/com/ptp/ptplatform/service/TrackerService.java
new file mode 100644
index 0000000..29bc95f
--- /dev/null
+++ b/src/main/java/com/ptp/ptplatform/service/TrackerService.java
@@ -0,0 +1,74 @@
+package com.ptp.ptplatform.service;
+
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.turn.ttorrent.tracker.Tracker;
+import jakarta.annotation.PostConstruct;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+//封装的对tracker的操作
+@Service
+public class TrackerService {
+
+    private Tracker tracker;
+
+    public void startTracker() throws Exception {
+        // 创建一个Tracker对象,监听端口6969
+        tracker = new Tracker(6969);
+
+        // 过滤目录下的.torrent文件
+        FilenameFilter filter = new FilenameFilter() {
+            @Override
+            public boolean accept(File dir, String name) {
+                return name.endsWith(".torrent");
+            }
+        };
+
+        // 获取当前项目根目录的路径并拼接torrents文件夹路径
+        String torrentDirectory = System.getProperty("user.dir") + File.separator + "torrents";
+        System.out.println("文件路径: " + torrentDirectory);
+
+        // 遍历目录中的所有torrent文件,传递给tracker
+        for (File file : new File(torrentDirectory).listFiles(filter)) {
+
+            TrackedTorrent tt = tracker.announce(TrackedTorrent.load(file));
+            System.out.println("种子文件: " + tt);
+        }
+
+        // 设置允许接收外部torrent
+        tracker.setAcceptForeignTorrents(true);
+
+        // 启动tracker服务
+        tracker.start(true);
+
+    }
+
+    public void stopTracker() {
+        if (tracker != null) {
+            tracker.stop();
+        }
+    }
+
+    public Collection<TrackedTorrent> getTrackedTorrents() {
+        return tracker.getTrackedTorrents();
+    }
+
+    public TrackedTorrent getTrackedTorrent(String hash) {
+        return tracker.getTrackedTorrent(hash);
+    }
+
+    public TrackedTorrent addTrackedTorrent(String filePath) throws IOException {
+        File file = new File(filePath);
+        return tracker.announce(TrackedTorrent.load(file));
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ptp/ptplatform/service/impl/TorrentServiceImpl.java b/src/main/java/com/ptp/ptplatform/service/impl/TorrentServiceImpl.java
index a6eba54..3281d83 100644
--- a/src/main/java/com/ptp/ptplatform/service/impl/TorrentServiceImpl.java
+++ b/src/main/java/com/ptp/ptplatform/service/impl/TorrentServiceImpl.java
@@ -1,24 +1,24 @@
 package com.ptp.ptplatform.service.impl;
 
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.ptp.ptplatform.entity.Torrent;
+import com.ptp.ptplatform.entity.TORRENT;
 import com.ptp.ptplatform.mapper.TorrentMapper;
 import com.ptp.ptplatform.service.TorrentService;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 @Service
-public class TorrentServiceImpl extends ServiceImpl<TorrentMapper, Torrent> implements TorrentService {
+public class TorrentServiceImpl extends ServiceImpl<TorrentMapper, TORRENT> implements TorrentService {
 
     @Override
-    public Torrent getTorrentById(Integer id) {
+    public TORRENT getTorrentById(Integer id) {
         return this.getById(id); // 直接调用 MyBatis-Plus 提供的 getById 方法
     }
     @Override
     @Transactional
     public void incrementLike(int postId) {
         this.update(null,
-                new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<Torrent>()
+                new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<TORRENT>()
                         .eq("id", postId)
                         .setSql("like_count = like_count + 1")
         );
@@ -28,7 +28,7 @@
     @Transactional
     public void incrementReplyCount(int postId) {
         this.update(null,
-                new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<Torrent>()
+                new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<TORRENT>()
                         .eq("id", postId)
                         .setSql("reply_count = reply_count + 1")
         );
diff --git a/src/main/java/com/ptp/ptplatform/utils/JwtUtils.java b/src/main/java/com/ptp/ptplatform/utils/JwtUtils.java
index 57bff31..0728819 100644
--- a/src/main/java/com/ptp/ptplatform/utils/JwtUtils.java
+++ b/src/main/java/com/ptp/ptplatform/utils/JwtUtils.java
@@ -1,13 +1,21 @@
 package com.ptp.ptplatform.utils;
 
+import com.ptp.ptplatform.entity.USER;
+import com.ptp.ptplatform.mapper.UserMapper;
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
 
 import javax.xml.crypto.Data;
 import java.util.Date;
 
+
 public class JwtUtils {
+
     private static long expire = 36000;
     private static String secret = "abcdefgg";
 
diff --git a/src/main/java/com/ptp/ptplatform/utils/Result.java b/src/main/java/com/ptp/ptplatform/utils/Result.java
index 0bda7eb..527fc42 100644
--- a/src/main/java/com/ptp/ptplatform/utils/Result.java
+++ b/src/main/java/com/ptp/ptplatform/utils/Result.java
@@ -2,6 +2,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
+//AAAAAAAAAAAAAaaaaa
 public class Result {
     private Boolean success;
     private Integer code;
@@ -74,4 +75,8 @@
         this.setData(map);
         return this;
     }
+
+    public boolean isSuccess(){
+        return success;
+    }
 }
diff --git a/src/main/java/com/ptp/ptplatform/utils/SizeCalculation.java b/src/main/java/com/ptp/ptplatform/utils/SizeCalculation.java
new file mode 100644
index 0000000..863e376
--- /dev/null
+++ b/src/main/java/com/ptp/ptplatform/utils/SizeCalculation.java
@@ -0,0 +1,9 @@
+package com.ptp.ptplatform.utils;
+
+//实现将字节大小转化为GB
+public class SizeCalculation {
+
+    public static int byteToGB(long bytes) {
+        return (int) (bytes / 1024 / 1024 / 1024);
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 730af5c..70338d3 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -26,4 +26,12 @@
 
 # MyBatis-Plus properties
 mybatis-plus.mapper-locations=classpath:/mapper/*.xml
-mybatis-plus.type-aliases-package=com.example.demo.model
\ No newline at end of file
+mybatis-plus.type-aliases-package=com.example.demo.model
+
+# Tracker??
+bittorrent.tracker.torrent-dir=./torrents
+
+# ??????
+spring.servlet.multipart.enabled=true
+spring.servlet.multipart.max-file-size=10MB
+spring.servlet.multipart.max-request-size=10MB
\ No newline at end of file
diff --git a/src/test/java/com/ptp/ptplatform/controller/InviteCodeTest.java b/src/test/java/com/ptp/ptplatform/controller/InviteCodeTest.java
new file mode 100644
index 0000000..9c000f3
--- /dev/null
+++ b/src/test/java/com/ptp/ptplatform/controller/InviteCodeTest.java
@@ -0,0 +1,116 @@
+package com.ptp.ptplatform.controller;
+
+import com.ptp.ptplatform.entity.INVITE_CODE;
+import com.ptp.ptplatform.entity.USER;
+import com.ptp.ptplatform.mapper.InviteCodeMapper;
+import com.ptp.ptplatform.mapper.UserMapper;
+import com.ptp.ptplatform.utils.JwtUtils;
+import com.ptp.ptplatform.utils.Result;
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.ResponseEntity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class InviteCodeTest {
+
+    @Mock
+    private InviteCodeMapper inviteCodeMapper;
+
+    @Mock
+    private UserMapper userMapper;
+
+    @Mock
+    private UserController userController;
+
+    @Mock
+    private HttpServletRequest request;
+
+    @InjectMocks
+    private InviteCodeController inviteCodeController;
+
+    private USER testUser;
+    private INVITE_CODE testInviteCode;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+
+        testUser = new USER();
+        testUser.setUsername("testUser");
+        testUser.setMagicPoints(15);
+
+        testInviteCode = new INVITE_CODE(testUser.getUsername());
+        testInviteCode.setCode("TESTCODE123");
+    }
+
+    @Test
+    void generateInviteCode_Success() {
+        // Arrange
+        when(userController.getUserInRequest(request)).thenReturn(testUser);
+        when(inviteCodeMapper.insertInviteCode(any(INVITE_CODE.class))).thenReturn(1);
+
+        // Act
+        Result result = inviteCodeController.generateInviteCode(request);
+
+        // Assert
+        assertTrue(result.isSuccess());
+        assertNotNull(result.getData().get("inviteCode"));
+        verify(userMapper, times(1)).updateUser(testUser);
+    }
+
+    @Test
+    void generateInviteCode_Fail_NotEnoughMagicPoints() {
+        // Arrange
+        testUser.setMagicPoints(5);
+        when(userController.getUserInRequest(request)).thenReturn(testUser);
+
+        // Act
+        Result result = inviteCodeController.generateInviteCode(request);
+
+        // Assert
+        assertFalse(result.isSuccess());
+        assertEquals("兑换邀请码失败,魔力值不足。", result.getMessage());
+        verify(inviteCodeMapper, never()).insertInviteCode(any());
+        verify(userMapper, never()).updateUser(any());
+    }
+
+    @Test
+    void userInviteCode_Success() {
+        // Arrange
+        List<INVITE_CODE> inviteCodes = new ArrayList<>();
+        inviteCodes.add(testInviteCode);
+
+        when(userController.getUserInRequest(request)).thenReturn(testUser);
+        when(inviteCodeMapper.selectByUser(testUser.getUsername())).thenReturn(inviteCodes);
+
+        // Act
+        Result result = inviteCodeController.userInviteCode(request);
+
+        // Assert
+        assertTrue(result.isSuccess());
+        assertEquals(inviteCodes, result.getData().get("inviteCode"));
+    }
+
+    @Test
+    void userInviteCode_EmptyList() {
+        // Arrange
+        when(userController.getUserInRequest(request)).thenReturn(testUser);
+        when(inviteCodeMapper.selectByUser(testUser.getUsername())).thenReturn(new ArrayList<>());
+
+        // Act
+        Result result = inviteCodeController.userInviteCode(request);
+
+        // Assert
+        assertTrue(result.isSuccess());
+        assertTrue(((List<?>) result.getData().get("inviteCode")).isEmpty());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/ptp/ptplatform/controller/TorrentCommentControllerTest.java b/src/test/java/com/ptp/ptplatform/controller/TORRENTCommentControllerTest.java
similarity index 99%
rename from src/test/java/com/ptp/ptplatform/controller/TorrentCommentControllerTest.java
rename to src/test/java/com/ptp/ptplatform/controller/TORRENTCommentControllerTest.java
index 585a546..40abc63 100644
--- a/src/test/java/com/ptp/ptplatform/controller/TorrentCommentControllerTest.java
+++ b/src/test/java/com/ptp/ptplatform/controller/TORRENTCommentControllerTest.java
@@ -16,7 +16,7 @@
 import static org.junit.jupiter.api.Assertions.*;

 import static org.mockito.Mockito.*;

 

-class TorrentCommentControllerTest {

+class TORRENTCommentControllerTest {

 

     @Mock

     private TorrentCommentService commentService;

diff --git a/src/test/java/com/ptp/ptplatform/controller/TORRENTControllerTest.java b/src/test/java/com/ptp/ptplatform/controller/TORRENTControllerTest.java
new file mode 100644
index 0000000..f383d5e
--- /dev/null
+++ b/src/test/java/com/ptp/ptplatform/controller/TORRENTControllerTest.java
@@ -0,0 +1,78 @@
+package com.ptp.ptplatform.controller;

+

+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

+import com.ptp.ptplatform.entity.TORRENT;

+import com.ptp.ptplatform.entity.TorrentComment;

+import com.ptp.ptplatform.service.TorrentCommentService;

+import com.ptp.ptplatform.service.TorrentService;

+import com.ptp.ptplatform.utils.Result;

+import org.junit.jupiter.api.BeforeEach;

+import org.junit.jupiter.api.Test;

+import org.mockito.InjectMocks;

+import org.mockito.Mock;

+import org.mockito.MockitoAnnotations;

+

+import java.time.LocalDateTime;

+import java.util.ArrayList;

+import java.util.List;

+

+import static org.junit.jupiter.api.Assertions.*;

+import static org.mockito.ArgumentMatchers.any;

+import static org.mockito.Mockito.*;

+

+class TORRENTControllerTest {

+

+    @Mock

+    private TorrentService torrentService;

+

+    @Mock

+    private TorrentCommentService commentService;

+

+    @InjectMocks

+    private TorrentController torrentController;

+

+    @BeforeEach

+    void setUp() {

+        MockitoAnnotations.openMocks(this);

+    }

+

+    @Test

+    void getTorrentById_ShouldReturnTorrent_WhenTorrentExists() {

+        // 1. 使用构造函数创建测试数据

+        TORRENT mockTORRENT = new TORRENT(

+                "d3b07384d113edec49eaa6238ad5ff00", // hash

+                "Ubuntu 22.04 ISO",                // torrentName

+                "Official Ubuntu Linux distribution", // description

+                "Software",                        // category

+                "美国",                            // region

+                "1080p",                          // resolution

+                "中文字幕",                        // subtitle

+                2048L,                            // size (2GB)

+                "admin",                          // username

+                "/downloads/ubuntu-22.04.iso"      // filePath

+        );

+        mockTORRENT.setId(1);

+        mockTORRENT.setLikeCount(200);

+

+        // 模拟空评论列表

+        List<TorrentComment> emptyComments = new ArrayList<>();

+

+        // 2. 模拟服务层行为

+        when(torrentService.getById(1)).thenReturn(mockTORRENT);

+        when(commentService.list(any(QueryWrapper.class))).thenReturn(emptyComments);

+

+        // 3. 调用控制器方法

+        Result result = torrentController.getPost(1);

+

+        // 4. 验证结果

+        assertEquals(200, result.getCode());

+        assertNotNull(result.getData().get("torrent"));

+        TORRENT returnedTORRENT = (TORRENT) result.getData().get("torrent");

+        assertEquals("Ubuntu 22.04 ISO", returnedTORRENT.getTorrentName());

+        assertEquals(200, returnedTORRENT.getLikeCount());

+

+        // 5. 验证服务层调用

+        verify(torrentService, times(1)).getById(1);

+        verify(commentService, times(1)).list(any(QueryWrapper.class));

+    }

+}
\ No newline at end of file
diff --git a/src/test/java/com/ptp/ptplatform/controller/TorrentControllerTest.java b/src/test/java/com/ptp/ptplatform/controller/TorrentControllerTest.java
deleted file mode 100644
index 9c4fbfa..0000000
--- a/src/test/java/com/ptp/ptplatform/controller/TorrentControllerTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.ptp.ptplatform.controller;

-

-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

-import com.ptp.ptplatform.entity.Torrent;

-import com.ptp.ptplatform.entity.TorrentComment;

-import com.ptp.ptplatform.service.TorrentCommentService;

-import com.ptp.ptplatform.service.TorrentService;

-import com.ptp.ptplatform.utils.Result;

-import org.junit.jupiter.api.BeforeEach;

-import org.junit.jupiter.api.Test;

-import org.mockito.InjectMocks;

-import org.mockito.Mock;

-import org.mockito.MockitoAnnotations;

-

-import java.time.LocalDateTime;

-import java.util.ArrayList;

-import java.util.List;

-

-import static org.junit.jupiter.api.Assertions.*;

-import static org.mockito.ArgumentMatchers.any;

-import static org.mockito.Mockito.*;

-

-class TorrentControllerTest {

-

-    @Mock

-    private TorrentService torrentService;

-

-    @Mock

-    private TorrentCommentService commentService;

-

-    @InjectMocks

-    private TorrentController torrentController;

-

-    @BeforeEach

-    void setUp() {

-        MockitoAnnotations.openMocks(this);

-    }

-

-    @Test

-    void getTorrentById_ShouldReturnTorrent_WhenTorrentExists() {

-        // 1. 构造测试数据

-        Torrent mockTorrent = new Torrent();

-        mockTorrent.setId(1);

-        mockTorrent.setTorrentName("Ubuntu 22.04 ISO");

-        mockTorrent.setDescription("Official Ubuntu Linux distribution");

-        mockTorrent.setCategory("Software");

-        mockTorrent.setFilePath("/downloads/ubuntu-22.04.iso");

-        mockTorrent.setCreateTime(LocalDateTime.of(2023, 5, 1, 10, 0));

-        mockTorrent.setRegion("美国");

-        mockTorrent.setLikeCount(200);

-

-        // 模拟空评论列表

-        List<TorrentComment> emptyComments = new ArrayList<>();

-

-        // 2. 模拟服务层行为

-        when(torrentService.getById(1)).thenReturn(mockTorrent);

-        when(commentService.list(any(QueryWrapper.class))).thenReturn(emptyComments);

-

-        // 3. 调用控制器方法

-        Result result = torrentController.getPost(1);

-

-        // 4. 验证结果

-        assertEquals(200, result.getCode());

-        assertNotNull(result.getData().get("torrent"));

-        Torrent returnedTorrent = (Torrent) result.getData().get("torrent");

-        assertEquals("Ubuntu 22.04 ISO", returnedTorrent.getTorrentName());

-        assertEquals(200, returnedTorrent.getLikeCount());

-

-        // 5. 验证服务层调用

-        verify(torrentService, times(1)).getById(1);

-        verify(commentService, times(1)).list(any(QueryWrapper.class));

-    }

-

-    @Test

-    void getTorrentDetails_ShouldReturnError_WhenTorrentNotExists() {

-        when(torrentService.getById(999)).thenReturn(null);

-

-        Result result = torrentController.getPost(999);

-

-        assertEquals(500, result.getCode());

-        assertEquals("种子不存在", result.getMessage());

-        verify(torrentService, times(1)).getById(999);

-        verify(commentService, never()).list(any(QueryWrapper.class));

-    }

-

-    @Test

-    void createTorrent_ShouldSuccess_WithValidData() {

-        Torrent newTorrent = new Torrent();

-        newTorrent.setTorrentName("New Torrent");

-        newTorrent.setDescription("Test creation");

-        newTorrent.setFilePath("/downloads/test.torrent");

-        newTorrent.setRegion("中国");

-

-        when(torrentService.save(any(Torrent.class))).thenReturn(true);

-

-        Result result = torrentController.createTorrent(newTorrent);

-

-        assertEquals(200, result.getCode());

-        assertEquals("成功", result.getMessage());

-        verify(torrentService, times(1)).save(any(Torrent.class));

-    }

-}
\ No newline at end of file
diff --git a/src/test/java/com/ptp/ptplatform/controller/UserControllerTest.java b/src/test/java/com/ptp/ptplatform/controller/UserControllerTest.java
index ebe2353..a413bda 100644
--- a/src/test/java/com/ptp/ptplatform/controller/UserControllerTest.java
+++ b/src/test/java/com/ptp/ptplatform/controller/UserControllerTest.java
@@ -6,13 +6,19 @@
 import com.ptp.ptplatform.mapper.UserMapper;
 import com.ptp.ptplatform.utils.JwtUtils;
 import com.ptp.ptplatform.utils.Result;
+import com.ptp.ptplatform.utils.SizeCalculation;
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.http.HttpServletRequest;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.MockedStatic;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
@@ -26,169 +32,249 @@
     private InviteCodeMapper inviteCodeMapper;
 
     @Mock
-    private JwtUtils jwtUtils;
+    private HttpServletRequest request;
 
     @InjectMocks
     private UserController userController;
 
+    private USER testUser;
+    private INVITE_CODE testInviteCode;
+
     @BeforeEach
     void setUp() {
         MockitoAnnotations.openMocks(this);
+
+        testUser = new USER();
+        testUser.setUsername("testUser");
+        testUser.setPassword("testPassword");
+        testUser.setLastLogin(new Date());
+        testUser.setUpload(1073741824L); // 1GB in bytes
+        testUser.setDownload(536870912L); // 0.5GB in bytes
+
+        testInviteCode = new INVITE_CODE("testUser");
+        testInviteCode.setCode("TESTCODE123");
+        testInviteCode.setIsUsed(false);
     }
 
-//    @Test
-//    void testLogin_Success() {
-//        // 准备测试数据
-//        String username = "testUser";
-//        String password = "testPass";
-//        String hashedPassword = "hashedTestPass";
-//        String token = "generatedToken";
-//
-//        // 创建 mock 对象
-//        USER mockUser = mock(USER.class);
-//        // 使用Mockito方式定义行为
-//        when(mockUser.getUsername()).thenReturn(username);
-//        when(mockUser.getPassword()).thenReturn(hashedPassword);
-//        // 如果 hashPassword 需要被调用
-//        when(mockUser.hashPassword(password)).thenReturn(hashedPassword);
-//
-//        // 模拟Mapper行为
-//        when(userMapper.selectByUsername(username)).thenReturn(mockUser);
-//        when(jwtUtils.generateToken(username)).thenReturn(token);
-//
-//        // 执行测试
-//        Result result = userController.login(username, password);
-//
-//        // 验证结果
-//        assertEquals(200, result.getCode());
-//        assertEquals(token, result.getData().get("token"));
-//        verify(userMapper, times(1)).selectByUsername(username);
-//        verify(jwtUtils, times(1)).generateToken(username);
-//    }
+    @Test
+    void info_Success() {
+        // Arrange
+        when(request.getHeader("Authorization")).thenReturn("validToken");
+
+        Claims mockClaims = mock(Claims.class);
+        when(mockClaims.getSubject()).thenReturn(testUser.getUsername());
+
+        try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
+            mockedJwtUtils.when(() -> JwtUtils.getClaimByToken("validToken")).thenReturn(mockClaims);
+            when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
+
+            // Act
+            Result result = userController.info(request);
+
+            // Assert
+            assertTrue(result.isSuccess());
+            assertEquals(testUser, result.getData().get("info"));
+        }
+    }
 
     @Test
-    void testLogin_UserNotFound() {
-        String username = "nonExistUser";
-        String password = "anyPass";
+    void login_Success() {
+        // Arrange
+        when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
 
-        when(userMapper.selectByUsername(username)).thenReturn(null);
+        // Act
+        Result result = userController.login(testUser.getUsername(), testUser.getPassword());
 
-        Result result = userController.login(username, password);
+        // Assert
+        assertTrue(result.isSuccess());
+        assertNotNull(result.getData().get("token"));
+        verify(userMapper, times(1)).updateUser(testUser);
+    }
 
-        assertEquals(500, result.getCode());
+    @Test
+    void login_Fail_WrongPassword() {
+        // Arrange
+        when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
+
+        // Act
+        Result result = userController.login(testUser.getUsername(), "wrongPassword");
+
+        // Assert
+        assertFalse(result.isSuccess());
+        assertEquals("密码错误", result.getMessage());
+        verify(userMapper, never()).updateUser(any());
+    }
+
+    @Test
+    void login_Fail_UserNotFound() {
+        // Arrange
+        when(userMapper.selectByUsername("nonexistentUser")).thenReturn(null);
+
+        // Act
+        Result result = userController.login("nonexistentUser", "anyPassword");
+
+        // Assert
+        assertFalse(result.isSuccess());
         assertEquals("用户不存在", result.getMessage());
     }
 
-//    @Test
-//    void testLogin_WrongPassword() {
-//        String username = "testUser";
-//        String password = "wrongPass";
-//        String hashedPassword = "hashedTestPass";
-//
-//        USER mockUser = new USER();
-//        mockUser.setUsername(username);
-//        mockUser.setPassword(hashedPassword);
-//
-//        when(userMapper.selectByUsername(username)).thenReturn(mockUser);
-//        when(mockUser.hashPassword(password)).thenReturn("wrongHash");
-//
-//        Result result = userController.login(username, password);
-//
-//        assertEquals(500, result.getCode());
-//        assertEquals("密码错误", result.getMessage());
-//    }
-
     @Test
-    void testRegister_Success() {
-        String username = "newUser";
-        String password = "newPass";
-        String code = "validCode";
-        Date now = new Date();
+    void regist_Success() {
+        // Arrange
+        when(userMapper.selectByUsername("newUser")).thenReturn(null);
+        when(inviteCodeMapper.selectByCode("VALIDCODE")).thenReturn(testInviteCode);
 
-        INVITE_CODE mockInviteCode = new INVITE_CODE();
-        mockInviteCode.setIsUsed(false);
+        // Act
+        Result result = userController.regist("newUser", "newPassword", "VALIDCODE");
 
-        when(userMapper.selectByUsername(username)).thenReturn(null);
-        when(inviteCodeMapper.selectByCode(code)).thenReturn(mockInviteCode);
-
-        Result result = userController.regist(username, password, code);
-
-        assertEquals(200, result.getCode());
+        // Assert
+        assertTrue(result.isSuccess());
         assertEquals("新建用户成功", result.getMessage());
         verify(userMapper, times(1)).insertUser(any(USER.class));
-        verify(inviteCodeMapper, times(1)).updateCodeUser(code);
+        verify(inviteCodeMapper, times(1)).updateCodeUser("VALIDCODE");
     }
 
     @Test
-    void testRegister_UsernameExists() {
-        String username = "existingUser";
-        String password = "anyPass";
-        String code = "anyCode";
+    void regist_Fail_UsernameExists() {
+        // Arrange
+        when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
 
-        USER existingUser = new USER();
-        when(userMapper.selectByUsername(username)).thenReturn(existingUser);
+        // Act
+        Result result = userController.regist(testUser.getUsername(), "anyPassword", "anyCode");
 
-        Result result = userController.regist(username, password, code);
-
-        assertEquals(500, result.getCode());
+        // Assert
+        assertFalse(result.isSuccess());
         assertEquals("用户名已存在,注册失败", result.getMessage());
         verify(userMapper, never()).insertUser(any());
     }
 
     @Test
-    void testRegister_InvalidCode() {
-        String username = "newUser";
-        String password = "newPass";
-        String code = "invalidCode";
+    void regist_Fail_InvalidCode() {
+        // Arrange
+        when(userMapper.selectByUsername("newUser")).thenReturn(null);
+        when(inviteCodeMapper.selectByCode("INVALIDCODE")).thenReturn(null);
 
-        when(userMapper.selectByUsername(username)).thenReturn(null);
-        when(inviteCodeMapper.selectByCode(code)).thenReturn(null);
+        // Act
+        Result result = userController.regist("newUser", "newPassword", "INVALIDCODE");
 
-        Result result = userController.regist(username, password, code);
-
-        assertEquals(500, result.getCode());
+        // Assert
+        assertFalse(result.isSuccess());
         assertEquals("邀请码不存在,注册失败", result.getMessage());
         verify(userMapper, never()).insertUser(any());
     }
 
     @Test
-    void testRegister_UsedCode() {
-        String username = "newUser";
-        String password = "newPass";
-        String code = "usedCode";
+    void regist_Fail_UsedCode() {
+        // Arrange
+        testInviteCode.setIsUsed(true);
+        when(userMapper.selectByUsername("newUser")).thenReturn(null);
+        when(inviteCodeMapper.selectByCode("USEDCODE")).thenReturn(testInviteCode);
 
-        INVITE_CODE usedCode = new INVITE_CODE();
-        usedCode.setIsUsed(true);
+        // Act
+        Result result = userController.regist("newUser", "newPassword", "USEDCODE");
 
-        when(userMapper.selectByUsername(username)).thenReturn(null);
-        when(inviteCodeMapper.selectByCode(code)).thenReturn(usedCode);
-
-        Result result = userController.regist(username, password, code);
-
-        assertEquals(500, result.getCode());
+        // Assert
+        assertFalse(result.isSuccess());
         assertEquals("邀请码已经被使用,注册失败", result.getMessage());
         verify(userMapper, never()).insertUser(any());
     }
 
-//    @Test
-//    void testGetUserInfo_Success() {
-//        String token = "validToken";
-//        String username = "testUser";
-//
-//        when(jwtUtils.getClaimByToken(token).getSubject()).thenReturn(username);
-//
-//        Result result = userController.info(token);
-//
-//        assertEquals(200, result.getCode());
-//        assertEquals(username, result.getData().get("name"));
-//    }
+    @Test
+    void allowDownload_Success() {
+        // Arrange
+        when(request.getHeader("Authorization")).thenReturn("validToken");
+
+        Claims mockClaims = mock(Claims.class);
+        when(mockClaims.getSubject()).thenReturn(testUser.getUsername());
+
+        try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
+            mockedJwtUtils.when(() -> JwtUtils.getClaimByToken("validToken")).thenReturn(mockClaims);
+            when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
+
+            // Act
+            Result result = userController.allowDownload(request);
+
+            // Assert
+            assertTrue(result.isSuccess());
+            Map<String, Object> data = result.getData();
+            assertEquals(1, data.get("totalSize")); // 1GB
+            assertEquals(0, data.get("usedSize")); // 0.5GB (but SizeCalculation.byteToGB rounds down)
+            assertEquals(1, data.get("leftSize")); // 0.5GB rounded up
+        }
+    }
 
     @Test
-    void testLogout() {
-        String token = "anyToken";
+    void updatePassword_Success() {
+        // Arrange
+        when(request.getHeader("Authorization")).thenReturn("validToken");
 
-        Result result = userController.logout(token);
+        Claims mockClaims = mock(Claims.class);
+        when(mockClaims.getSubject()).thenReturn(testUser.getUsername());
 
-        assertEquals(200, result.getCode());
+        try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
+            mockedJwtUtils.when(() -> JwtUtils.getClaimByToken("validToken")).thenReturn(mockClaims);
+
+            when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
+
+            Map<String, String> passwordMap = new HashMap<>();
+            passwordMap.put("oldPassword", testUser.getPassword());
+            passwordMap.put("newPassword", "newPassword123");
+
+            // Act
+            Result result = userController.updatePassword(request, passwordMap);
+
+            // Assert
+            assertTrue(result.isSuccess());
+            assertEquals("修改密码成功", result.getMessage());
+            verify(userMapper, times(1)).updateUser(testUser);
+        }
+    }
+
+    @Test
+    void updatePassword_Fail_WrongOldPassword() {
+        // Arrange
+        when(request.getHeader("Authorization")).thenReturn("validToken");
+
+        // 创建模拟的 Claims 对象
+        Claims mockClaims = mock(Claims.class);
+        when(mockClaims.getSubject()).thenReturn(testUser.getUsername());  // 模拟 getSubject() 返回用户名
+
+        try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
+            // 模拟 JwtUtils.getClaimByToken 返回模拟的 Claims 对象
+            mockedJwtUtils.when(() -> JwtUtils.getClaimByToken("validToken")).thenReturn(mockClaims);
+
+            when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
+
+            Map<String, String> passwordMap = new HashMap<>();
+            passwordMap.put("oldPassword", "wrongPassword");
+            passwordMap.put("newPassword", "newPassword123");
+
+            // Act
+            Result result = userController.updatePassword(request, passwordMap);
+
+            // Assert
+            assertFalse(result.isSuccess());
+            assertEquals("原密码不正确", result.getMessage());
+            verify(userMapper, never()).updateUser(any());
+        }
+    }
+
+    @Test
+    void getUserInRequest_Success() {
+        // Arrange
+        when(request.getHeader("Authorization")).thenReturn("validToken");
+        Claims mockClaims = mock(Claims.class);
+        when(mockClaims.getSubject()).thenReturn(testUser.getUsername());  // 模拟 getSubject() 返回用户名
+
+        try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
+            mockedJwtUtils.when(() -> JwtUtils.getClaimByToken("validToken")).thenReturn(mockClaims);
+            when(userMapper.selectByUsername(testUser.getUsername())).thenReturn(testUser);
+
+            // Act
+            USER result = userController.getUserInRequest(request);
+
+            // Assert
+            assertEquals(testUser, result);
+        }
     }
 }
\ No newline at end of file
diff --git a/ttorrent-master/.gitignore b/ttorrent-master/.gitignore
new file mode 100644
index 0000000..f77703a
--- /dev/null
+++ b/ttorrent-master/.gitignore
@@ -0,0 +1,25 @@
+# Git ignore patterns
+# Author: Maxime Petazzoni <mpetazzoni@turn.com>
+
+# Ignore build output
+/build/*
+*/build
+
+# Ignore Javadoc output
+/doc/*
+
+# Ignore any eventual Eclipse project files, these don't belong in the
+# repository.
+/.classpath
+/.project
+/.settings
+/.idea/workspace.xml
+# Ignore common editor swap files
+*.swp
+*.bak
+*~
+*~
+
+#ignore idea workspace file and idea module files
+*.iml
+.idea/workspace.xml
diff --git a/ttorrent-master/COPYING b/ttorrent-master/COPYING
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/ttorrent-master/COPYING
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/ttorrent-master/INSTALL b/ttorrent-master/INSTALL
new file mode 100644
index 0000000..d33121e
--- /dev/null
+++ b/ttorrent-master/INSTALL
@@ -0,0 +1,28 @@
+Howto build and use the BitTorrent library
+==========================================
+
+Dependencies
+------------
+
+This Java implementation of the BitTorrent protocol implements a BitTorrent
+tracker (an HTTP service), and a BitTorrent client. The only dependencies of
+the BitTorrent library are:
+
+* the log4j library
+* the slf4j logging library
+* the SimpleHTTPFramework
+
+These libraries are provided in the lib/ directory, and are automatically
+included in the JAR file created by the build process.
+
+
+Building the distribution JAR
+-----------------------------
+
+Simply execute the following command:
+
+  $ mvn package
+
+To build the library's JAR file (in the target/ directory). You can then import
+this JAR file into your Java project and start using the Java BitTorrent
+library.
diff --git a/ttorrent-master/README.md b/ttorrent-master/README.md
new file mode 100644
index 0000000..99ef3c8
--- /dev/null
+++ b/ttorrent-master/README.md
@@ -0,0 +1,153 @@
+[![JetBrains team project](http://jb.gg/badges/team.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
+
+Ttorrent, a Java implementation of the BitTorrent protocol
+==========================================================
+
+#### Note
+It's Ttorrent library version 2.0 which has
+a lot of improvements and may not be compatible with previous version
+(stored in [v1.6 branch](https://github.com/mpetazzoni/ttorrent/tree/v1.6))
+
+See [this issue](https://github.com/mpetazzoni/ttorrent/issues/212) for details 
+
+Description
+-----------
+
+**Ttorrent** is a pure-Java implementation of the BitTorrent protocol,
+providing a BitTorrent tracker, a BitTorrent client and the related Torrent
+metainfo files creation and parsing capabilities. It is designed to be embedded
+into larger applications, but its components can also be used as standalone
+programs.
+
+Ttorrent supports the following BEPs (BitTorrent enhancement proposals):
+
+* `BEP#0003`: The BitTorrent protocol specification  
+  This is the base official protocol specification, which Ttorrent implements
+  fully.
+* `BEP#0012`: Multi-tracker metadata extension  
+  Full support for the `announce-list` meta-info key providing a tiered tracker
+  list.
+* `BEP#0015`: UDP Tracker Protocol for BitTorrent  
+  The UDP tracker protocol is fully supported in the BitTorrent client to make
+  announce requests to UDP trackers. UDP tracker support itself is planned.
+* `BEP#0020`: Peer ID conventions  
+  Ttorrent uses `TO` as the client identification string, and currently uses
+  the `-T00042-` client ID prefix.
+* `BEP#0023`: Tracker Returns Compact Peer Lists  
+  Compact peer lists are supported in both the client and the tracker.
+  Currently the tracker only supports sending back compact peer lists
+  to an announce request.
+
+History
+-------
+
+This tool suite was implemented as part of Turn's (http://www.turn.com) release
+distribution and deployment system and is used to distribute new build tarballs
+to a large number of machines inside a datacenter as efficiently as possible.
+At the time this project was started, few Java implementations of the
+BitTorrent protocol existed and unfortunately none of them fit our needs:
+
+* Vuze's, which is very hard to extract from their codebase, and thus complex
+to re-integrate into another application;
+* torrent4j, which is largely incomplete and not usable;
+* Snark's, which is old, and unfortunately unstable;
+* bitext, which was also unfortunately unstable, and extremely slow.
+
+This implementation aims at providing a down-to-earth, simple to use library.
+No fancy protocol extensions are implemented here: just the basics that allows
+for the exchange and distribution of files through the BitTorrent protocol.
+
+How to use
+----------
+
+### As a library
+
+#### Client code
+
+```java
+// First, instantiate the Client object.
+SimpleClient client = new SimpleClient();
+
+// This is the interface the client will listen on (you might need something
+// else than localhost here because other peers cannot connect to localhost).
+InetAddress address = InetAddress.getLocalHost();
+
+//Start download. Thread is blocked here
+try {
+  client.downloadTorrent("/path/to/filed.torrent",
+          "/path/to/output/directory",
+          address);
+  //download finished
+} catch (Exception e) {
+  //download failed, see exception for details
+  e.printStackTrace();
+}
+//If you don't want to seed the torrent you can stop client
+client.stop();
+```
+
+#### Tracker code
+
+```java
+// First, instantiate a Tracker object with the port you want it to listen on.
+// The default tracker port recommended by the BitTorrent protocol is 6969.
+Tracker tracker = new Tracker(6969);
+
+// Then, for each torrent you wish to announce on this tracker, simply created
+// a TrackedTorrent object and pass it to the tracker.announce() method:
+FilenameFilter filter = new FilenameFilter() {
+  @Override
+  public boolean accept(File dir, String name) {
+    return name.endsWith(".torrent");
+  }
+};
+
+for (File f : new File("/path/to/torrent/files").listFiles(filter)) {
+  tracker.announce(TrackedTorrent.load(f));
+}
+
+//Also you can enable accepting foreign torrents.
+//if tracker accepts request for unknown torrent it starts tracking the torrent automatically
+tracker.setAcceptForeignTorrents(true);
+
+// Once done, you just have to start the tracker's main operation loop:
+tracker.start(true);
+
+// You can stop the tracker when you're done with:
+tracker.stop();
+```
+
+License
+-------
+
+This BitTorrent library is distributed under the terms of the Apache Software
+License version 2.0. See COPYING file for more details.
+
+
+Authors and contributors
+------------------------
+
+* Maxime Petazzoni <<mpetazzoni@turn.com>> (Platform Engineer at Turn, Inc)  
+  Original author, main developer and maintainer
+* David Giffin <<david@etsy.com>>  
+  Contributed parallel hashing and multi-file torrent support.
+* Thomas Zink <<thomas.zink@uni-konstanz.de>>  
+  Fixed a piece length computation issue when the total torrent size is an
+  exact multiple of the piece size.
+* Johan Parent <<parent_johan@yahoo.com>>  
+  Fixed a bug in unfresh peer collection and issues on download completion on
+  Windows platforms.
+* Dmitriy Dumanskiy  
+  Contributed the switch from Ant to Maven.
+* Alexey Ptashniy  
+  Fixed an integer overflow in the calculation of a torrent's full size.
+
+
+Caveats
+-------
+
+* Client write performance is a bit poor, mainly due to a (too?) simple piece
+  caching algorithm.
+
+Contributions are welcome in all areas, even more so for these few points
+above!
diff --git a/ttorrent-master/assembly.xml b/ttorrent-master/assembly.xml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ttorrent-master/assembly.xml
diff --git a/ttorrent-master/bencoding/pom.xml b/ttorrent-master/bencoding/pom.xml
new file mode 100644
index 0000000..95e628a
--- /dev/null
+++ b/ttorrent-master/bencoding/pom.xml
@@ -0,0 +1,17 @@
+<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/bencoding</name>
+    <url>http://turn.github.com/ttorrent/</url>
+    <artifactId>ttorrent-bencoding</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java
new file mode 100644
index 0000000..8a2b3ab
--- /dev/null
+++ b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BDecoder.java
@@ -0,0 +1,320 @@
+/**
+ * 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.bcodec;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * B-encoding decoder.
+ *
+ * <p>
+ * A b-encoded byte stream can represent byte arrays, numbers, lists and maps
+ * (dictionaries). This class implements a decoder of such streams into
+ * {@link BEValue}s.
+ * </p>
+ *
+ * <p>
+ * Inspired by Snark's implementation.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @see <a href="http://en.wikipedia.org/wiki/Bencode">B-encoding specification</a>
+ */
+public class BDecoder {
+
+  // The InputStream to BDecode.
+  private final InputStream in;
+
+  // The last indicator read.
+  // Zero if unknown.
+  // '0'..'9' indicates a byte[].
+  // 'i' indicates an Number.
+  // 'l' indicates a List.
+  // 'd' indicates a Map.
+  // 'e' indicates end of Number, List or Map (only used internally).
+  // -1 indicates end of stream.
+  // Call getNextIndicator to get the current value (will never return zero).
+  private int indicator = 0;
+
+  /**
+   * Initializes a new BDecoder.
+   *
+   * <p>
+   * Nothing is read from the given <code>InputStream</code> yet.
+   * </p>
+   *
+   * @param in The input stream to read from.
+   */
+  public BDecoder(InputStream in) {
+    this.in = in;
+  }
+
+  /**
+   * Decode a B-encoded stream.
+   *
+   * <p>
+   * Automatically instantiates a new BDecoder for the provided input stream
+   * and decodes its root member.
+   * </p>
+   *
+   * @param in The input stream to read from.
+   */
+  public static BEValue bdecode(InputStream in) throws IOException {
+    return new BDecoder(in).bdecode();
+  }
+
+  /**
+   * Decode a B-encoded byte buffer.
+   *
+   * <p>
+   * Automatically instantiates a new BDecoder for the provided buffer and
+   * decodes its root member.
+   * </p>
+   *
+   * @param data The {@link ByteBuffer} to read from.
+   */
+  public static BEValue bdecode(ByteBuffer data) throws IOException {
+    return BDecoder.bdecode(new ByteArrayInputStream(data.array()));
+  }
+
+  /**
+   * Returns what the next b-encoded object will be on the stream or -1
+   * when the end of stream has been reached.
+   *
+   * <p>
+   * Can return something unexpected (not '0' .. '9', 'i', 'l' or 'd') when
+   * the stream isn't b-encoded.
+   * </p>
+   *
+   * This might or might not read one extra byte from the stream.
+   */
+  private int getNextIndicator() throws IOException {
+    if (this.indicator == 0) {
+      this.indicator = in.read();
+    }
+    return this.indicator;
+  }
+
+  /**
+   * Gets the next indicator and returns either null when the stream
+   * has ended or b-decodes the rest of the stream and returns the
+   * appropriate BEValue encoded object.
+   */
+  public BEValue bdecode() throws IOException {
+    if (this.getNextIndicator() == -1)
+      return null;
+
+    if (this.indicator >= '0' && this.indicator <= '9')
+      return this.bdecodeBytes();
+    else if (this.indicator == 'i')
+      return this.bdecodeNumber();
+    else if (this.indicator == 'l')
+      return this.bdecodeList();
+    else if (this.indicator == 'd')
+      return this.bdecodeMap();
+    else
+      throw new InvalidBEncodingException
+              ("Unknown indicator '" + this.indicator + "'");
+  }
+
+  /**
+   * Returns the next b-encoded value on the stream and makes sure it is a
+   * byte array.
+   *
+   * @throws InvalidBEncodingException If it is not a b-encoded byte array.
+   */
+  public BEValue bdecodeBytes() throws IOException {
+    int c = this.getNextIndicator();
+    int num = c - '0';
+    if (num < 0 || num > 9)
+      throw new InvalidBEncodingException("Number expected, not '"
+              + (char) c + "'");
+    this.indicator = 0;
+
+    c = this.read();
+    int i = c - '0';
+    while (i >= 0 && i <= 9) {
+      // This can overflow!
+      num = num * 10 + i;
+      c = this.read();
+      i = c - '0';
+    }
+
+    if (c != ':') {
+      throw new InvalidBEncodingException("Colon expected, not '" +
+              (char) c + "'");
+    }
+
+    return new BEValue(read(num));
+  }
+
+  /**
+   * Returns the next b-encoded value on the stream and makes sure it is a
+   * number.
+   *
+   * @throws InvalidBEncodingException If it is not a number.
+   */
+  public BEValue bdecodeNumber() throws IOException {
+    int c = this.getNextIndicator();
+    if (c != 'i') {
+      throw new InvalidBEncodingException("Expected 'i', not '" +
+              (char) c + "'");
+    }
+    this.indicator = 0;
+
+    c = this.read();
+    if (c == '0') {
+      c = this.read();
+      if (c == 'e')
+        return new BEValue(BigInteger.ZERO);
+      else
+        throw new InvalidBEncodingException("'e' expected after zero," +
+                " not '" + (char) c + "'");
+    }
+
+    // We don't support more the 255 char big integers
+    char[] chars = new char[256];
+    int off = 0;
+
+    if (c == '-') {
+      c = this.read();
+      if (c == '0')
+        throw new InvalidBEncodingException("Negative zero not allowed");
+      chars[off] = '-';
+      off++;
+    }
+
+    if (c < '1' || c > '9')
+      throw new InvalidBEncodingException("Invalid Integer start '"
+              + (char) c + "'");
+    chars[off] = (char) c;
+    off++;
+
+    c = this.read();
+    int i = c - '0';
+    while (i >= 0 && i <= 9) {
+      chars[off] = (char) c;
+      off++;
+      c = read();
+      i = c - '0';
+    }
+
+    if (c != 'e')
+      throw new InvalidBEncodingException("Integer should end with 'e'");
+
+    String s = new String(chars, 0, off);
+    return new BEValue(new BigInteger(s));
+  }
+
+  /**
+   * Returns the next b-encoded value on the stream and makes sure it is a
+   * list.
+   *
+   * @throws InvalidBEncodingException If it is not a list.
+   */
+  public BEValue bdecodeList() throws IOException {
+    int c = this.getNextIndicator();
+    if (c != 'l') {
+      throw new InvalidBEncodingException("Expected 'l', not '" +
+              (char) c + "'");
+    }
+    this.indicator = 0;
+
+    List<BEValue> result = new ArrayList<BEValue>();
+    c = this.getNextIndicator();
+    while (c != 'e') {
+      result.add(this.bdecode());
+      c = this.getNextIndicator();
+    }
+    this.indicator = 0;
+
+    return new BEValue(result);
+  }
+
+  /**
+   * Returns the next b-encoded value on the stream and makes sure it is a
+   * map (dictionary).
+   *
+   * @throws InvalidBEncodingException If it is not a map.
+   */
+  public BEValue bdecodeMap() throws IOException {
+    int c = this.getNextIndicator();
+    if (c != 'd') {
+      throw new InvalidBEncodingException("Expected 'd', not '" +
+              (char) c + "'");
+    }
+    this.indicator = 0;
+
+    Map<String, BEValue> result = new HashMap<String, BEValue>();
+    c = this.getNextIndicator();
+    while (c != 'e') {
+      // Dictionary keys are always strings.
+      String key = this.bdecode().getString();
+
+      BEValue value = this.bdecode();
+      result.put(key, value);
+
+      c = this.getNextIndicator();
+    }
+    this.indicator = 0;
+
+    return new BEValue(result);
+  }
+
+  /**
+   * Returns the next byte read from the InputStream (as int).
+   *
+   * @throws EOFException If InputStream.read() returned -1.
+   */
+  private int read() throws IOException {
+    int c = this.in.read();
+    if (c == -1)
+      throw new EOFException();
+    return c;
+  }
+
+  /**
+   * Returns a byte[] containing length valid bytes starting at offset zero.
+   *
+   * @throws EOFException If InputStream.read() returned -1 before all
+   *                      requested bytes could be read.  Note that the byte[] returned might be
+   *                      bigger then requested but will only contain length valid bytes.  The
+   *                      returned byte[] will be reused when this method is called again.
+   */
+  private byte[] read(int length) throws IOException {
+    byte[] result = new byte[length];
+
+    int read = 0;
+    while (read < length) {
+      int i = this.in.read(result, read, length - read);
+      if (i == -1)
+        throw new EOFException();
+      read += i;
+    }
+
+    return result;
+  }
+}
diff --git a/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BEValue.java b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BEValue.java
new file mode 100644
index 0000000..875bcf1
--- /dev/null
+++ b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BEValue.java
@@ -0,0 +1,184 @@
+/**
+ * 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.bcodec;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * A type-agnostic container for B-encoded values.
+ *
+ * b加密方法,bt种子文件中存储数据的规则
+ * @author mpetazzoni
+ */
+public class BEValue {
+
+  /**
+   * The B-encoded value can be a byte array, a Number, a List or a Map.
+   * Lists and Maps contains BEValues too.
+   */
+  private final Object value;
+
+  public BEValue(byte[] value) {
+    this.value = value;
+  }
+
+  public BEValue(String value) throws UnsupportedEncodingException {
+    this.value = value.getBytes("UTF-8");
+  }
+
+  public BEValue(String value, String enc)
+          throws UnsupportedEncodingException {
+    this.value = value.getBytes(enc);
+  }
+
+  public BEValue(int value) {
+    this.value = new Integer(value);
+  }
+
+  public BEValue(long value) {
+    this.value = new Long(value);
+  }
+
+  public BEValue(Number value) {
+    this.value = value;
+  }
+
+  public BEValue(List<BEValue> value) {
+    this.value = value;
+  }
+
+  public BEValue(Map<String, BEValue> value) {
+    this.value = value;
+  }
+
+  public Object getValue() {
+    return this.value;
+  }
+
+  /**
+   * Returns this BEValue as a String, interpreted as UTF-8.
+   *
+   * @throws InvalidBEncodingException If the value is not a byte[].
+   */
+  public String getString() throws InvalidBEncodingException {
+    return this.getString("UTF-8");
+  }
+
+  /**
+   * Returns this BEValue as a String, interpreted with the specified
+   * encoding.
+   *
+   * @param encoding The encoding to interpret the bytes as when converting
+   *                 them into a {@link String}.
+   * @throws InvalidBEncodingException If the value is not a byte[].
+   */
+  public String getString(String encoding) throws InvalidBEncodingException {
+    try {
+      return new String(this.getBytes(), encoding);
+    } catch (ClassCastException cce) {
+      throw new InvalidBEncodingException(cce.toString());
+    } catch (UnsupportedEncodingException uee) {
+      throw new InternalError(uee.toString());
+    }
+  }
+
+  /**
+   * Returns this BEValue as a byte[].
+   *
+   * @throws InvalidBEncodingException If the value is not a byte[].
+   */
+  public byte[] getBytes() throws InvalidBEncodingException {
+    try {
+      return (byte[]) this.value;
+    } catch (ClassCastException cce) {
+      throw new InvalidBEncodingException(cce.toString());
+    }
+  }
+
+  /**
+   * Returns this BEValue as a Number.
+   *
+   * @throws InvalidBEncodingException If the value is not a {@link Number}.
+   */
+  public Number getNumber() throws InvalidBEncodingException {
+    try {
+      return (Number) this.value;
+    } catch (ClassCastException cce) {
+      throw new InvalidBEncodingException(cce.toString());
+    }
+  }
+
+  /**
+   * Returns this BEValue as short.
+   *
+   * @throws InvalidBEncodingException If the value is not a {@link Number}.
+   */
+  public short getShort() throws InvalidBEncodingException {
+    return this.getNumber().shortValue();
+  }
+
+  /**
+   * Returns this BEValue as int.
+   *
+   * @throws InvalidBEncodingException If the value is not a {@link Number}.
+   */
+  public int getInt() throws InvalidBEncodingException {
+    return this.getNumber().intValue();
+  }
+
+  /**
+   * Returns this BEValue as long.
+   *
+   * @throws InvalidBEncodingException If the value is not a {@link Number}.
+   */
+  public long getLong() throws InvalidBEncodingException {
+    return this.getNumber().longValue();
+  }
+
+  /**
+   * Returns this BEValue as a List of BEValues.
+   *
+   * @throws InvalidBEncodingException If the value is not an
+   *                                   {@link ArrayList}.
+   */
+  @SuppressWarnings("unchecked")
+  public List<BEValue> getList() throws InvalidBEncodingException {
+    if (this.value instanceof ArrayList) {
+      return (ArrayList<BEValue>) this.value;
+    } else {
+      throw new InvalidBEncodingException("Excepted List<BEvalue> !");
+    }
+  }
+
+  /**
+   * Returns this BEValue as a Map of String keys and BEValue values.
+   *
+   * @throws InvalidBEncodingException If the value is not a {@link HashMap}.
+   */
+  @SuppressWarnings("unchecked")
+  public Map<String, BEValue> getMap() throws InvalidBEncodingException {
+    if (this.value instanceof HashMap) {
+      return (Map<String, BEValue>) this.value;
+    } else {
+      throw new InvalidBEncodingException("Expected Map<String, BEValue> !");
+    }
+  }
+}
diff --git a/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BEncoder.java b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BEncoder.java
new file mode 100644
index 0000000..0619720
--- /dev/null
+++ b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/BEncoder.java
@@ -0,0 +1,117 @@
+/**
+ * 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.bcodec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.*;
+
+/**
+ * B-encoding encoder.
+ *
+ * <p>
+ * This class provides utility methods to encode objects and
+ * {@link BEValue}s to B-encoding into a provided output stream.
+ * </p>
+ *
+ * <p>
+ * Inspired by Snark's implementation.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @see <a href="http://en.wikipedia.org/wiki/Bencode">B-encoding specification</a>
+ */
+public class BEncoder {
+
+  @SuppressWarnings("unchecked")
+  public static void bencode(Object o, OutputStream out)
+          throws IOException, IllegalArgumentException {
+    if (o instanceof BEValue) {
+      o = ((BEValue) o).getValue();
+    }
+
+    if (o instanceof String) {
+      bencode((String) o, out);
+    } else if (o instanceof byte[]) {
+      bencode((byte[]) o, out);
+    } else if (o instanceof Number) {
+      bencode((Number) o, out);
+    } else if (o instanceof List) {
+      bencode((List<BEValue>) o, out);
+    } else if (o instanceof Map) {
+      bencode((Map<String, BEValue>) o, out);
+    } else {
+      throw new IllegalArgumentException("Cannot bencode: " +
+              o.getClass());
+    }
+  }
+
+  public static void bencode(String s, OutputStream out) throws IOException {
+    byte[] bs = s.getBytes("UTF-8");
+    bencode(bs, out);
+  }
+
+  public static void bencode(Number n, OutputStream out) throws IOException {
+    out.write('i');
+    String s = n.toString();
+    out.write(s.getBytes("UTF-8"));
+    out.write('e');
+  }
+
+  public static void bencode(List<BEValue> l, OutputStream out)
+          throws IOException {
+    out.write('l');
+    for (BEValue value : l) {
+      bencode(value, out);
+    }
+    out.write('e');
+  }
+
+  public static void bencode(byte[] bs, OutputStream out) throws IOException {
+    String l = Integer.toString(bs.length);
+    out.write(l.getBytes("UTF-8"));
+    out.write(':');
+    out.write(bs);
+  }
+
+  public static void bencode(Map<String, BEValue> m, OutputStream out)
+          throws IOException {
+    out.write('d');
+
+    // Keys must be sorted.
+    Set<String> s = m.keySet();
+    List<String> l = new ArrayList<String>(s);
+    Collections.sort(l);
+
+    for (String key : l) {
+      Object value = m.get(key);
+      bencode(key, out);
+      bencode(value, out);
+    }
+
+    out.write('e');
+  }
+
+  public static ByteBuffer bencode(Map<String, BEValue> m)
+          throws IOException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    BEncoder.bencode(m, baos);
+    baos.close();
+    return ByteBuffer.wrap(baos.toByteArray());
+  }
+}
diff --git a/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/InvalidBEncodingException.java b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/InvalidBEncodingException.java
new file mode 100644
index 0000000..8a0d1dd
--- /dev/null
+++ b/ttorrent-master/bencoding/src/main/java/com/turn/ttorrent/bcodec/InvalidBEncodingException.java
@@ -0,0 +1,34 @@
+/**
+ * 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.bcodec;
+
+import java.io.IOException;
+
+
+/**
+ * Exception thrown when a B-encoded stream cannot be decoded.
+ *
+ * @author mpetazzoni
+ */
+public class InvalidBEncodingException extends IOException {
+
+  public static final long serialVersionUID = -1;
+
+  public InvalidBEncodingException(String message) {
+    super(message);
+  }
+}
diff --git a/ttorrent-master/bencoding/src/test/java/BDecoderTest.java b/ttorrent-master/bencoding/src/test/java/BDecoderTest.java
new file mode 100644
index 0000000..b48ef69
--- /dev/null
+++ b/ttorrent-master/bencoding/src/test/java/BDecoderTest.java
@@ -0,0 +1,56 @@
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+public class BDecoderTest {
+
+  @Test
+  public void testDecodeNumbers() throws IOException {
+
+    testNumber(0);
+    testNumber(1);
+    testNumber(Integer.MAX_VALUE);
+    testNumber(1234567);
+    testNumber(-1);
+    testNumber(-100);
+    testNumber(Integer.MIN_VALUE);
+    testNumber(Long.MAX_VALUE);
+    testNumber(Long.MIN_VALUE);
+
+    //by specification number with lead zero it's incorrect value
+    testBadNumber("00");
+    testBadNumber("01234");
+    testBadNumber("000");
+    testBadNumber("0001");
+
+  }
+
+  private void testBadNumber(String number) throws IOException {
+    try {
+      BDecoder.bdecode(numberToBEPBytes(number));
+    } catch (InvalidBEncodingException e) {
+      return;
+    }
+    fail("Value " + number + " is incorrect by BEP specification but is was parsed correctly");
+  }
+
+  private void testNumber(long value) throws IOException {
+    assertEquals(BDecoder.bdecode(numberToBEPBytes(value)).getLong(), value);
+  }
+
+  private ByteBuffer numberToBEPBytes(long value) throws UnsupportedEncodingException {
+    return ByteBuffer.wrap(("i" + value + "e").getBytes("ASCII"));
+  }
+
+  private ByteBuffer numberToBEPBytes(String value) throws UnsupportedEncodingException {
+    return ByteBuffer.wrap(("i" + value + "e").getBytes("ASCII"));
+  }
+
+}
diff --git a/ttorrent-master/bin/ttorent-torrent b/ttorrent-master/bin/ttorent-torrent
new file mode 100644
index 0000000..4193c4e
--- /dev/null
+++ b/ttorrent-master/bin/ttorent-torrent
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# 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.
+
+EXEFILE="${0%-torrent}"
+MAINCLASS="com.turn.ttorrent.cli.TorrentMain" "${EXEFILE}" "$@"
diff --git a/ttorrent-master/bin/ttorrent b/ttorrent-master/bin/ttorrent
new file mode 100644
index 0000000..fc21dea
--- /dev/null
+++ b/ttorrent-master/bin/ttorrent
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+# 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.
+
+EXECJAR="ttorrent-*-shaded.jar"
+
+real_path() {
+    case $1 in
+        /*)
+            SCRIPT="$1"
+            ;;
+        *)
+            PWD=`pwd`
+            SCRIPT="$PWD/$1"
+            ;;
+    esac
+    CHANGED=true
+    while [ "X$CHANGED" != "X" ] ; do
+        # Change spaces to ":" so the tokens can be parsed.
+        SAFESCRIPT=`echo $SCRIPT | sed -e 's; ;:;g'`
+        # Get the real path to this script, resolving any symbolic links
+        TOKENS=`echo $SAFESCRIPT | sed -e 's;/; ;g'`
+        REALPATH=
+        for C in $TOKENS; do
+            # Change any ":" in the token back to a space.
+            C=`echo $C | sed -e 's;:; ;g'`
+            REALPATH="$REALPATH/$C"
+            # If REALPATH is a sym link, resolve it.  Loop for nested links.
+            while [ -h "$REALPATH" ] ; do
+                LS="`ls -ld "$REALPATH"`"
+                LINK="`expr "$LS" : '.*-> \(.*\)$'`"
+                if expr "$LINK" : '/.*' > /dev/null; then
+                    # LINK is absolute.
+                    REALPATH="$LINK"
+                else
+                    # LINK is relative.
+                    REALPATH="`dirname "$REALPATH"`""/$LINK"
+                fi
+            done
+        done
+        if [ "$REALPATH" = "$SCRIPT" ] ; then
+            CHANGED=""
+        else
+            SCRIPT="$REALPATH"
+        fi
+    done
+    echo "$REALPATH"
+}
+
+base=$(dirname $(real_path $0))
+CPARG=$(find ${base}/../build -name "$EXECJAR" | tail -n 1)
+if [ -z "$CPARG" ] ; then
+    echo "Unable to find $EXECJAR"
+    exit 1
+fi
+if [ -z "$MAINCLASS" ] ; then
+    CPARG="-jar $CPARG"
+else
+    CPARG="-cp $CPARG $MAINCLASS"
+fi
+exec java $CPARG "$@"
diff --git a/ttorrent-master/bin/ttorrent-tracker b/ttorrent-master/bin/ttorrent-tracker
new file mode 100644
index 0000000..e6bacd5
--- /dev/null
+++ b/ttorrent-master/bin/ttorrent-tracker
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# 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.
+
+EXEFILE="${0%-tracker}"
+MAINCLASS="com.turn.ttorrent.cli.TrackerMain" "${EXEFILE}" "$@"
diff --git a/ttorrent-master/cli/pom.xml b/ttorrent-master/cli/pom.xml
new file mode 100644
index 0000000..0e0b6c9
--- /dev/null
+++ b/ttorrent-master/cli/pom.xml
@@ -0,0 +1,71 @@
+<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>
+	</parent>
+
+	<name>Java BitTorrent library CLI</name>
+	<artifactId>ttorrent-cli</artifactId>
+	<packaging>jar</packaging>
+
+	<dependencies>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-client</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-tracker</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+	</dependencies>
+
+	<build>
+		<defaultGoal>package</defaultGoal>
+
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<version>2.4</version>
+				<configuration>
+					<archive>
+						<manifest>
+							<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+						</manifest>
+					</archive>
+					<includes>
+						<include>**</include>
+					</includes>
+				</configuration>
+			</plugin>
+
+			<plugin>
+				<artifactId>maven-shade-plugin</artifactId>
+				<version>2.1</version>
+				<executions>
+					<execution>
+						<phase>package</phase>
+						<goals>
+							<goal>shade</goal>
+						</goals>
+						<configuration>
+							<outputFile>${project.build.directory}/${project.artifactId}-${project.version}-shaded.jar</outputFile>
+							<transformers>
+								<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+									<manifestEntries>
+										<Main-Class>com.turn.ttorrent.cli.ClientMain</Main-Class>
+									</manifestEntries>
+								</transformer>
+							</transformers>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/ClientMain.java b/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/ClientMain.java
new file mode 100644
index 0000000..e7a68d1
--- /dev/null
+++ b/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/ClientMain.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (C) 2011-2013 Turn, Inc.
+ * <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.cli;
+
+import com.turn.ttorrent.client.CommunicationManager;
+import com.turn.ttorrent.client.SimpleClient;
+import jargs.gnu.CmdLineParser;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.PatternLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.net.*;
+import java.nio.channels.UnsupportedAddressTypeException;
+import java.util.Enumeration;
+
+/**
+ * Command-line entry-point for starting a {@link CommunicationManager}
+ */
+public class ClientMain {
+
+  private static final Logger logger =
+          LoggerFactory.getLogger(ClientMain.class);
+
+  /**
+   * Default data output directory.
+   */
+  private static final String DEFAULT_OUTPUT_DIRECTORY = "/tmp";
+
+  /**
+   * Returns a usable {@link Inet4Address} for the given interface name.
+   *
+   * <p>
+   * If an interface name is given, return the first usable IPv4 address for
+   * that interface. If no interface name is given or if that interface
+   * doesn't have an IPv4 address, return's localhost address (if IPv4).
+   * </p>
+   *
+   * <p>
+   * It is understood this makes the client IPv4 only, but it is important to
+   * remember that most BitTorrent extensions (like compact peer lists from
+   * trackers and UDP tracker support) are IPv4-only anyway.
+   * </p>
+   *
+   * @param iface The network interface name.
+   * @return A usable IPv4 address as a {@link Inet4Address}.
+   * @throws UnsupportedAddressTypeException If no IPv4 address was available
+   * to bind on.
+   */
+  private static Inet4Address getIPv4Address(String iface)
+          throws SocketException, UnsupportedAddressTypeException,
+          UnknownHostException {
+    if (iface != null) {
+      Enumeration<InetAddress> addresses =
+              NetworkInterface.getByName(iface).getInetAddresses();
+      while (addresses.hasMoreElements()) {
+        InetAddress addr = addresses.nextElement();
+        if (addr instanceof Inet4Address) {
+          return (Inet4Address) addr;
+        }
+      }
+    }
+
+    InetAddress localhost = InetAddress.getLocalHost();
+    if (localhost instanceof Inet4Address) {
+      return (Inet4Address) localhost;
+    }
+
+    throw new UnsupportedAddressTypeException();
+  }
+
+  /**
+   * Display program usage on the given {@link PrintStream}.
+   */
+  private static void usage(PrintStream s) {
+    s.println("usage: Client [options] <torrent>");
+    s.println();
+    s.println("Available options:");
+    s.println("  -h,--help                  Show this help and exit.");
+    s.println("  -o,--output DIR            Read/write data to directory DIR.");
+    s.println("  -i,--iface IFACE           Bind to interface IFACE.");
+    s.println("  -s,--seed SECONDS          Time to seed after downloading (default: infinitely).");
+    s.println("  -d,--max-download KB/SEC   Max download rate (default: unlimited).");
+    s.println("  -u,--max-upload KB/SEC     Max upload rate (default: unlimited).");
+    s.println();
+  }
+
+  /**
+   * Main client entry point for stand-alone operation.
+   */
+  public static void main(String[] args) {
+    BasicConfigurator.configure(new ConsoleAppender(
+            new PatternLayout("%d [%-25t] %-5p: %m%n")));
+
+    CmdLineParser parser = new CmdLineParser();
+    CmdLineParser.Option help = parser.addBooleanOption('h', "help");
+    CmdLineParser.Option output = parser.addStringOption('o', "output");
+    CmdLineParser.Option iface = parser.addStringOption('i', "iface");
+    CmdLineParser.Option seedTime = parser.addIntegerOption('s', "seed");
+    CmdLineParser.Option maxUpload = parser.addDoubleOption('u', "max-upload");
+    CmdLineParser.Option maxDownload = parser.addDoubleOption('d', "max-download");
+
+    try {
+      parser.parse(args);
+    } catch (CmdLineParser.OptionException oe) {
+      System.err.println(oe.getMessage());
+      usage(System.err);
+      System.exit(1);
+    }
+
+    // Display help and exit if requested
+    if (Boolean.TRUE.equals((Boolean) parser.getOptionValue(help))) {
+      usage(System.out);
+      System.exit(0);
+    }
+
+    String outputValue = (String) parser.getOptionValue(output,
+            DEFAULT_OUTPUT_DIRECTORY);
+    String ifaceValue = (String) parser.getOptionValue(iface);
+    int seedTimeValue = (Integer) parser.getOptionValue(seedTime, -1);
+
+    String[] otherArgs = parser.getRemainingArgs();
+    if (otherArgs.length != 1) {
+      usage(System.err);
+      System.exit(1);
+    }
+
+    SimpleClient client = new SimpleClient();
+    try {
+      Inet4Address iPv4Address = getIPv4Address(ifaceValue);
+      File torrentFile = new File(otherArgs[0]);
+      File outputFile = new File(outputValue);
+
+      client.downloadTorrent(
+              torrentFile.getAbsolutePath(),
+              outputFile.getAbsolutePath(),
+              iPv4Address);
+      if (seedTimeValue > 0) {
+        Thread.sleep(seedTimeValue * 1000);
+      }
+
+    } catch (Exception e) {
+      logger.error("Fatal error: {}", e.getMessage(), e);
+      System.exit(2);
+    } finally {
+      client.stop();
+    }
+  }
+}
diff --git a/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/TorrentMain.java b/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/TorrentMain.java
new file mode 100644
index 0000000..4ab6353
--- /dev/null
+++ b/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/TorrentMain.java
@@ -0,0 +1,190 @@
+/*
+  Copyright (C) 2011-2013 Turn, Inc.
+  <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.cli;
+
+
+import com.turn.ttorrent.common.TorrentCreator;
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentParser;
+import com.turn.ttorrent.common.TorrentSerializer;
+import jargs.gnu.CmdLineParser;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.filefilter.TrueFileFilter;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.PatternLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * Command-line entry-point for reading and writing {@link TorrentMetadata}.
+ */
+public class TorrentMain {
+
+  private static final Logger logger =
+          LoggerFactory.getLogger(TorrentMain.class);
+
+  /**
+   * Display program usage on the given {@link PrintStream}.
+   */
+  private static void usage(PrintStream s) {
+    usage(s, null);
+  }
+
+  /**
+   * Display a message and program usage on the given {@link PrintStream}.
+   */
+  private static void usage(PrintStream s, String msg) {
+    if (msg != null) {
+      s.println(msg);
+      s.println();
+    }
+
+    s.println("usage: Torrent [options] [file|directory]");
+    s.println();
+    s.println("Available options:");
+    s.println("  -h,--help             Show this help and exit.");
+    s.println("  -t,--torrent FILE     Use FILE to read/write torrent file.");
+    s.println();
+    s.println("  -c,--create           Create a new torrent file using " +
+            "the given announce URL and data.");
+    s.println("  -l,--length           Define the piece length for hashing data");
+    s.println("  -a,--announce         Tracker URL (can be repeated).");
+    s.println();
+  }
+
+  /**
+   * Torrent reader and creator.
+   *
+   * <p>
+   * You can use the {@code main()} function of this class to read or create
+   * torrent files. See usage for details.
+   * </p>
+   */
+  public static void main(String[] args) {
+    BasicConfigurator.configure(new ConsoleAppender(
+            new PatternLayout("%-5p: %m%n")));
+
+    CmdLineParser parser = new CmdLineParser();
+    CmdLineParser.Option help = parser.addBooleanOption('h', "help");
+    CmdLineParser.Option filename = parser.addStringOption('t', "torrent");
+    CmdLineParser.Option create = parser.addBooleanOption('c', "create");
+    CmdLineParser.Option pieceLength = parser.addIntegerOption('l', "length");
+    CmdLineParser.Option announce = parser.addStringOption('a', "announce");
+
+    try {
+      parser.parse(args);
+    } catch (CmdLineParser.OptionException oe) {
+      System.err.println(oe.getMessage());
+      usage(System.err);
+      System.exit(1);
+    }
+
+    // Display help and exit if requested
+    if (Boolean.TRUE.equals(parser.getOptionValue(help))) {
+      usage(System.out);
+      System.exit(0);
+    }
+
+    String filenameValue = (String) parser.getOptionValue(filename);
+    if (filenameValue == null) {
+      usage(System.err, "Torrent file must be provided!");
+      System.exit(1);
+    }
+
+    Integer pieceLengthVal = (Integer) parser.getOptionValue(pieceLength);
+    if (pieceLengthVal == null) {
+      pieceLengthVal = TorrentCreator.DEFAULT_PIECE_LENGTH;
+    } else {
+      pieceLengthVal = pieceLengthVal * 1024;
+    }
+    logger.info("Using piece length of {} bytes.", pieceLengthVal);
+
+    Boolean createFlag = (Boolean) parser.getOptionValue(create);
+
+    //For repeated announce urls
+    @SuppressWarnings("unchecked")
+    Vector<String> announceURLs = (Vector<String>) parser.getOptionValues(announce);
+
+    String[] otherArgs = parser.getRemainingArgs();
+
+    if (Boolean.TRUE.equals(createFlag) &&
+            (otherArgs.length != 1 || announceURLs.isEmpty())) {
+      usage(System.err, "Announce URL and a file or directory must be " +
+              "provided to create a torrent file!");
+      System.exit(1);
+    }
+
+    OutputStream fos = null;
+    try {
+      if (Boolean.TRUE.equals(createFlag)) {
+        fos = new FileOutputStream(filenameValue);
+
+        //Process the announce URLs into URIs
+        List<URI> announceURIs = new ArrayList<URI>();
+        for (String url : announceURLs) {
+          announceURIs.add(new URI(url));
+        }
+
+        //Create the announce-list as a list of lists of URIs
+        //Assume all the URI's are first tier trackers
+        List<List<URI>> announceList = new ArrayList<List<URI>>();
+        announceList.add(announceURIs);
+
+        File source = new File(otherArgs[0]);
+        if (!source.exists() || !source.canRead()) {
+          throw new IllegalArgumentException(
+                  "Cannot access source file or directory " +
+                          source.getName());
+        }
+
+        String creator = String.format("%s (ttorrent)",
+                System.getProperty("user.name"));
+
+        TorrentMetadata torrent;
+        if (source.isDirectory()) {
+          List<File> files = new ArrayList<File>(FileUtils.listFiles(source, TrueFileFilter.TRUE, TrueFileFilter.TRUE));
+          Collections.sort(files);
+          torrent = TorrentCreator.create(source, files, announceList.get(0).get(0), announceList, creator, pieceLengthVal);
+        } else {
+          torrent = TorrentCreator.create(source, null, announceList.get(0).get(0), announceList, creator, pieceLengthVal);
+        }
+
+        fos.write(new TorrentSerializer().serialize(torrent));
+      } else {
+        new TorrentParser().parseFromFile(new File(filenameValue));
+      }
+    } catch (Exception e) {
+      logger.error("{}", e.getMessage(), e);
+      System.exit(2);
+    } finally {
+      if (fos != System.out) {
+        IOUtils.closeQuietly(fos);
+      }
+    }
+  }
+}
diff --git a/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/TrackerMain.java b/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/TrackerMain.java
new file mode 100644
index 0000000..0556e94
--- /dev/null
+++ b/ttorrent-master/cli/src/main/java/com/turn/ttorrent/cli/TrackerMain.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (C) 2011-2013 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.cli;
+
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.turn.ttorrent.tracker.Tracker;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.PrintStream;
+import java.net.InetSocketAddress;
+
+import jargs.gnu.CmdLineParser;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.PatternLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Command-line entry-point for starting a {@link Tracker}
+ */
+public class TrackerMain {
+
+	private static final Logger logger =
+		LoggerFactory.getLogger(TrackerMain.class);
+
+	/**
+	 * Display program usage on the given {@link PrintStream}.
+	 */
+	private static void usage(PrintStream s) {
+		s.println("usage: Tracker [options] [directory]");
+		s.println();
+		s.println("Available options:");
+		s.println("  -h,--help             Show this help and exit.");
+		s.println("  -p,--port PORT        Bind to port PORT.");
+		s.println();
+	}
+
+	/**
+	 * Main function to start a tracker.
+	 */
+	public static void main(String[] args) {
+		BasicConfigurator.configure(new ConsoleAppender(
+			new PatternLayout("%d [%-25t] %-5p: %m%n")));
+
+		CmdLineParser parser = new CmdLineParser();
+		CmdLineParser.Option help = parser.addBooleanOption('h', "help");
+		CmdLineParser.Option port = parser.addIntegerOption('p', "port");
+
+		try {
+			parser.parse(args);
+		} catch (CmdLineParser.OptionException oe) {
+			System.err.println(oe.getMessage());
+			usage(System.err);
+			System.exit(1);
+		}
+
+		// Display help and exit if requested
+		if (Boolean.TRUE.equals((Boolean)parser.getOptionValue(help))) {
+			usage(System.out);
+			System.exit(0);
+		}
+
+		Integer portValue = (Integer)parser.getOptionValue(port,
+			Integer.valueOf(Tracker.DEFAULT_TRACKER_PORT));
+
+		String[] otherArgs = parser.getRemainingArgs();
+
+		if (otherArgs.length > 1) {
+			usage(System.err);
+			System.exit(1);
+		}
+
+		// Get directory from command-line argument or default to current
+		// directory
+		String directory = otherArgs.length > 0
+			? otherArgs[0]
+			: ".";
+
+		FilenameFilter filter = new FilenameFilter() {
+			@Override
+			public boolean accept(File dir, String name) {
+				return name.endsWith(".torrent");
+			}
+		};
+
+		try {
+			Tracker t = new Tracker(portValue);
+
+			File parent = new File(directory);
+			for (File f : parent.listFiles(filter)) {
+				logger.info("Loading torrent from " + f.getName());
+				t.announce(TrackedTorrent.load(f));
+			}
+
+			logger.info("Starting tracker with {} announced torrents...",
+				t.getTrackedTorrents().size());
+			t.start(true);
+		} catch (Exception e) {
+			logger.error("{}", e.getMessage(), e);
+			System.exit(2);
+		}
+	}
+}
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);
+    }
+  }
+}
diff --git a/ttorrent-master/network/pom.xml b/ttorrent-master/network/pom.xml
new file mode 100644
index 0000000..72c994b
--- /dev/null
+++ b/ttorrent-master/network/pom.xml
@@ -0,0 +1,35 @@
+<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/network</name>
+    <url>http://turn.github.com/ttorrent/</url>
+    <artifactId>ttorrent-network</artifactId>
+    <version>1.0</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-common</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-test-api</artifactId>
+            <version>1.0</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/AcceptAttachment.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/AcceptAttachment.java
new file mode 100644
index 0000000..acba652
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/AcceptAttachment.java
@@ -0,0 +1,10 @@
+package com.turn.ttorrent.network;
+
+public interface AcceptAttachment {
+
+  /**
+   * @return channel listener factory for create listeners for new connections
+   */
+  ChannelListenerFactory getChannelListenerFactory();
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/AcceptAttachmentImpl.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/AcceptAttachmentImpl.java
new file mode 100644
index 0000000..ce0b051
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/AcceptAttachmentImpl.java
@@ -0,0 +1,32 @@
+package com.turn.ttorrent.network;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+
+public class AcceptAttachmentImpl implements AcceptAttachment, TimeoutAttachment {
+
+  private final ChannelListenerFactory myChannelListenerFactory;
+
+  public AcceptAttachmentImpl(ChannelListenerFactory channelListenerFactory) {
+    this.myChannelListenerFactory = channelListenerFactory;
+  }
+
+  @Override
+  public ChannelListenerFactory getChannelListenerFactory() {
+    return myChannelListenerFactory;
+  }
+
+  @Override
+  public boolean isTimeoutElapsed(long currentTimeMillis) {
+    return false;//accept attachment doesn't closed by timeout
+  }
+
+  @Override
+  public void communicatedNow(long currentTimeMillis) {
+  }
+
+  @Override
+  public void onTimeoutElapsed(SocketChannel channel) throws IOException {
+
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ChannelListenerFactory.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ChannelListenerFactory.java
new file mode 100644
index 0000000..25e111c
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ChannelListenerFactory.java
@@ -0,0 +1,7 @@
+package com.turn.ttorrent.network;
+
+public interface ChannelListenerFactory {
+
+  ConnectionListener newChannelListener();
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectTask.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectTask.java
new file mode 100644
index 0000000..5906873
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectTask.java
@@ -0,0 +1,59 @@
+package com.turn.ttorrent.network;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.nio.channels.SocketChannel;
+
+public class ConnectTask implements TimeoutAttachment, ReadAttachment {
+
+  private long lastCommunicationTime;
+  private final int myTimeoutMillis;
+  private final String myHost;
+  private final int myPort;
+  private final ConnectionListener myConnectionListener;
+
+  public ConnectTask(String host, int port, ConnectionListener connectionListener, long lastCommunicationTime, int timeoutMillis) {
+    this.myHost = host;
+    this.myPort = port;
+    this.myConnectionListener = connectionListener;
+    this.myTimeoutMillis = timeoutMillis;
+    this.lastCommunicationTime = lastCommunicationTime;
+  }
+
+  public String getHost() {
+    return myHost;
+  }
+
+  public int getPort() {
+    return myPort;
+  }
+
+  @Override
+  public ConnectionListener getConnectionListener() {
+    return myConnectionListener;
+  }
+
+  @Override
+  public String toString() {
+    return "ConnectTask{" +
+            "myHost='" + myHost + '\'' +
+            ", myPort=" + myPort +
+            '}';
+  }
+
+  @Override
+  public boolean isTimeoutElapsed(long currentTimeMillis) {
+    long minTimeForKeepAlive = currentTimeMillis - myTimeoutMillis;
+    return minTimeForKeepAlive > lastCommunicationTime;
+  }
+
+  @Override
+  public void communicatedNow(long currentTimeMillis) {
+    lastCommunicationTime = currentTimeMillis;
+  }
+
+  @Override
+  public void onTimeoutElapsed(SocketChannel channel) throws IOException {
+    myConnectionListener.onError(channel, new SocketTimeoutException());
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionClosedException.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionClosedException.java
new file mode 100644
index 0000000..f6dca57
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionClosedException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.network;
+
+import java.io.IOException;
+
+public class ConnectionClosedException extends IOException {
+
+  public ConnectionClosedException() {
+  }
+
+  public ConnectionClosedException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionListener.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionListener.java
new file mode 100644
index 0000000..09f5888
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionListener.java
@@ -0,0 +1,32 @@
+package com.turn.ttorrent.network;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+
+public interface ConnectionListener {
+
+  /**
+   * invoked when specified socket channel contains any data
+   *
+   * @param socketChannel specified socket channel with data
+   * @throws IOException if an I/O error occurs
+   */
+  void onNewDataAvailable(SocketChannel socketChannel) throws IOException;
+
+  /**
+   * invoked when get new connection
+   *
+   * @param socketChannel specified socket channel
+   * @throws IOException if an I/O error occurs
+   */
+  void onConnectionEstablished(SocketChannel socketChannel) throws IOException;
+
+  /**
+   * invoked when an error occurs
+   *
+   * @param socketChannel specified channel, associated with this channel
+   * @param ex            specified exception
+   * @throws IOException if an I/O error occurs
+   */
+  void onError(SocketChannel socketChannel, Throwable ex) throws IOException;
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionManager.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionManager.java
new file mode 100644
index 0000000..76700a8
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionManager.java
@@ -0,0 +1,175 @@
+package com.turn.ttorrent.network;
+
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.TimeService;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.keyProcessors.*;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.Channel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.util.Arrays;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.turn.ttorrent.Constants.DEFAULT_CLEANUP_RUN_TIMEOUT_MILLIS;
+import static com.turn.ttorrent.Constants.DEFAULT_SELECTOR_SELECT_TIMEOUT_MILLIS;
+
+public class ConnectionManager {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(ConnectionManager.class);
+
+  private final Selector selector;
+  private final TimeService myTimeService;
+  private volatile ConnectionWorker myConnectionWorker;
+  private int myBindPort;
+  private final ConnectionManagerContext myContext;
+  private volatile ServerSocketChannel myServerSocketChannel;
+  private volatile Future<?> myWorkerFuture;
+  private final NewConnectionAllower myIncomingConnectionAllower;
+  private final NewConnectionAllower myOutgoingConnectionAllower;
+  private final TimeoutStorage socketTimeoutStorage = new TimeoutStorageImpl();
+  private final AtomicBoolean alreadyInit = new AtomicBoolean(false);
+  private final AtomicInteger mySendBufferSize;
+  private final AtomicInteger myReceiveBufferSize;
+
+  public ConnectionManager(ConnectionManagerContext context,
+                           TimeService timeService,
+                           NewConnectionAllower newIncomingConnectionAllower,
+                           NewConnectionAllower newOutgoingConnectionAllower,
+                           SelectorFactory selectorFactory,
+                           AtomicInteger mySendBufferSize,
+                           AtomicInteger myReceiveBufferSize) throws IOException {
+    this.mySendBufferSize = mySendBufferSize;
+    this.myReceiveBufferSize = myReceiveBufferSize;
+    this.selector = selectorFactory.newSelector();
+    this.myTimeService = timeService;
+    myContext = context;
+    this.myIncomingConnectionAllower = newIncomingConnectionAllower;
+    this.myOutgoingConnectionAllower = newOutgoingConnectionAllower;
+  }
+
+  public void initAndRunWorker(ServerChannelRegister serverChannelRegister) throws IOException {
+
+    boolean wasInit = alreadyInit.getAndSet(true);
+
+    if (wasInit) {
+      throw new IllegalStateException("connection manager was already initialized");
+    }
+
+    myServerSocketChannel = serverChannelRegister.channelFor(selector);
+    myServerSocketChannel.register(selector, SelectionKey.OP_ACCEPT, new AcceptAttachmentImpl(myContext));
+    myBindPort = myServerSocketChannel.socket().getLocalPort();
+    String serverName = myServerSocketChannel.socket().toString();
+    myConnectionWorker = new ConnectionWorker(selector, Arrays.asList(
+            new InvalidKeyProcessor(),
+            new AcceptableKeyProcessor(selector, serverName, myTimeService, myIncomingConnectionAllower, socketTimeoutStorage,
+                    mySendBufferSize, myReceiveBufferSize),
+            new ConnectableKeyProcessor(selector, myTimeService, socketTimeoutStorage,
+                    mySendBufferSize, myReceiveBufferSize),
+            new ReadableKeyProcessor(serverName),
+            new WritableKeyProcessor()), DEFAULT_SELECTOR_SELECT_TIMEOUT_MILLIS, DEFAULT_CLEANUP_RUN_TIMEOUT_MILLIS,
+            myTimeService,
+            new CleanupKeyProcessor(myTimeService),
+            myOutgoingConnectionAllower);
+    myWorkerFuture = myContext.getExecutor().submit(myConnectionWorker);
+  }
+
+  public void setSelectorSelectTimeout(int timeout) {
+    ConnectionWorker workerLocal = myConnectionWorker;
+    checkThatWorkerIsInit(workerLocal);
+    workerLocal.setSelectorSelectTimeout(timeout);
+  }
+
+  private void checkThatWorkerIsInit(ConnectionWorker worker) {
+    if (worker == null) throw new IllegalStateException("Connection manager is not initialized!");
+  }
+
+  public boolean offerConnect(ConnectTask connectTask, int timeout, TimeUnit timeUnit) {
+    if (myConnectionWorker == null) {
+      return false;
+    }
+    return myConnectionWorker.offerConnect(connectTask, timeout, timeUnit);
+  }
+
+  public boolean offerWrite(WriteTask writeTask, int timeout, TimeUnit timeUnit) {
+    if (myConnectionWorker == null) {
+      return false;
+    }
+    return myConnectionWorker.offerWrite(writeTask, timeout, timeUnit);
+  }
+
+
+  public int getBindPort() {
+    return myBindPort;
+  }
+
+  public void close(int timeout, TimeUnit timeUnit) {
+    logger.debug("try close connection manager...");
+    boolean successfullyClosed = true;
+    if (myConnectionWorker != null) {
+      myWorkerFuture.cancel(true);
+      try {
+        boolean shutdownCorrectly = myConnectionWorker.stop(timeout, timeUnit);
+        if (!shutdownCorrectly) {
+          successfullyClosed = false;
+          logger.warn("unable to terminate worker in {} {}", timeout, timeUnit);
+        }
+      } catch (InterruptedException e) {
+        successfullyClosed = false;
+        LoggerUtils.warnAndDebugDetails(logger, "unable to await termination worker, thread was interrupted", e);
+      }
+    }
+    try {
+      this.myServerSocketChannel.close();
+    } catch (Throwable e) {
+      LoggerUtils.errorAndDebugDetails(logger, "unable to close server socket channel", e);
+      successfullyClosed = false;
+    }
+    for (SelectionKey key : this.selector.keys()) {
+      try {
+        if (key.isValid()) {
+          key.channel().close();
+        }
+      } catch (Throwable e) {
+        logger.error("unable to close socket channel {}", key.channel());
+        successfullyClosed = false;
+        logger.debug("", e);
+      }
+    }
+    try {
+      this.selector.close();
+    } catch (Throwable e) {
+      LoggerUtils.errorAndDebugDetails(logger, "unable to close selector channel", e);
+      successfullyClosed = false;
+    }
+    if (successfullyClosed) {
+      logger.debug("connection manager is successfully closed");
+    } else {
+      logger.error("connection manager wasn't closed successfully");
+    }
+  }
+
+  public void close() {
+    close(1, TimeUnit.MINUTES);
+  }
+
+  public void setCleanupTimeout(long timeoutMillis) {
+    ConnectionWorker workerLocal = myConnectionWorker;
+    checkThatWorkerIsInit(workerLocal);
+    workerLocal.setCleanupTimeout(timeoutMillis);
+  }
+
+  public void setSocketConnectionTimeout(long timeoutMillis) {
+    socketTimeoutStorage.setTimeout(timeoutMillis);
+  }
+
+  public void closeChannel(Channel channel) throws IOException {
+    channel.close();
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionManagerContext.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionManagerContext.java
new file mode 100644
index 0000000..c95cbb5
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionManagerContext.java
@@ -0,0 +1,9 @@
+package com.turn.ttorrent.network;
+
+import java.util.concurrent.ExecutorService;
+
+public interface ConnectionManagerContext extends ChannelListenerFactory {
+
+  ExecutorService getExecutor();
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionWorker.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionWorker.java
new file mode 100644
index 0000000..2d83275
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ConnectionWorker.java
@@ -0,0 +1,259 @@
+package com.turn.ttorrent.network;
+
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.TimeService;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.keyProcessors.CleanupProcessor;
+import com.turn.ttorrent.network.keyProcessors.KeyProcessor;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.channels.*;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectionWorker implements Runnable {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(ConnectionWorker.class);
+  private static final String SELECTOR_THREAD_NAME = "Torrent channels manager thread";
+  private volatile boolean stop = false;
+  private final Selector selector;
+  private final BlockingQueue<ConnectTask> myConnectQueue;
+  private final BlockingQueue<WriteTask> myWriteQueue;
+  private final Semaphore mySemaphore;
+  private final List<KeyProcessor> myKeyProcessors;
+  private final TimeService myTimeService;
+  private long lastCleanupTime;
+  private volatile int mySelectorTimeoutMillis;
+  private volatile long myCleanupTimeoutMillis;
+  private final CleanupProcessor myCleanupProcessor;
+  private final NewConnectionAllower myNewConnectionAllower;
+
+  ConnectionWorker(Selector selector,
+                   List<KeyProcessor> keyProcessors,
+                   int selectorTimeoutMillis,
+                   int cleanupTimeoutMillis,
+                   TimeService timeService,
+                   CleanupProcessor cleanupProcessor,
+                   NewConnectionAllower myNewConnectionAllower) {
+    this.selector = selector;
+    this.myTimeService = timeService;
+    this.lastCleanupTime = timeService.now();
+    this.mySelectorTimeoutMillis = selectorTimeoutMillis;
+    this.myCleanupTimeoutMillis = cleanupTimeoutMillis;
+    this.myCleanupProcessor = cleanupProcessor;
+    this.myNewConnectionAllower = myNewConnectionAllower;
+    this.mySemaphore = new Semaphore(1);
+    this.myConnectQueue = new LinkedBlockingQueue<ConnectTask>(100);
+    this.myKeyProcessors = keyProcessors;
+    this.myWriteQueue = new LinkedBlockingQueue<WriteTask>(5000);
+  }
+
+  @Override
+  public void run() {
+
+    try {
+      mySemaphore.acquire();
+    } catch (InterruptedException e) {
+      return;
+    }
+
+    final String oldName = Thread.currentThread().getName();
+
+    try {
+
+      Thread.currentThread().setName(SELECTOR_THREAD_NAME);
+
+      while (!stop && (!Thread.currentThread().isInterrupted())) {
+        try {
+          logger.trace("try select keys from selector");
+          int selected;
+          try {
+            selected = selector.select(mySelectorTimeoutMillis);
+          } catch (ClosedSelectorException e) {
+            break;
+          }
+          connectToPeersFromQueue();
+          processWriteTasks();
+          logger.trace("select keys from selector. Keys count is " + selected);
+          if (selected != 0) {
+            processSelectedKeys();
+          }
+          if (needRunCleanup()) {
+            cleanup();
+          }
+        } catch (Throwable e) {
+          LoggerUtils.warnAndDebugDetails(logger, "unable to select channel keys. Error message {}", e.getMessage(), e);
+        }
+      }
+    } catch (Throwable e) {
+      LoggerUtils.errorAndDebugDetails(logger, "exception on cycle iteration", e);
+    } finally {
+      Thread.currentThread().setName(oldName);
+      mySemaphore.release();
+    }
+  }
+
+  private void cleanup() {
+    lastCleanupTime = myTimeService.now();
+    for (SelectionKey key : selector.keys()) {
+      if (!key.isValid()) continue;
+      myCleanupProcessor.processCleanup(key);
+    }
+  }
+
+  private boolean needRunCleanup() {
+    return (myTimeService.now() - lastCleanupTime) > myCleanupTimeoutMillis;
+  }
+
+  private void processWriteTasks() {
+
+    final Iterator<WriteTask> iterator = myWriteQueue.iterator();
+    while (iterator.hasNext()) {
+      WriteTask writeTask = iterator.next();
+      if (stop || Thread.currentThread().isInterrupted()) {
+        return;
+      }
+      logger.trace("try register channel for write. Write task is {}", writeTask);
+      SocketChannel socketChannel = (SocketChannel) writeTask.getSocketChannel();
+      if (!socketChannel.isOpen()) {
+        iterator.remove();
+        writeTask.getListener().onWriteFailed(getDefaultWriteErrorMessageWithSuffix(socketChannel, "Channel is not open"), new ConnectionClosedException());
+        continue;
+      }
+      SelectionKey key = socketChannel.keyFor(selector);
+      if (key == null) {
+        logger.warn("unable to find key for channel {}", socketChannel);
+        iterator.remove();
+        writeTask.getListener().onWriteFailed(getDefaultWriteErrorMessageWithSuffix(socketChannel, "Can not find key for the channel"), new ConnectionClosedException());
+        continue;
+      }
+      Object attachment = key.attachment();
+      if (!(attachment instanceof WriteAttachment)) {
+        logger.error("incorrect attachment {} for channel {}", attachment, socketChannel);
+        iterator.remove();
+        writeTask.getListener().onWriteFailed(getDefaultWriteErrorMessageWithSuffix(socketChannel, "Incorrect attachment instance for the key"), new ConnectionClosedException());
+        continue;
+      }
+      WriteAttachment keyAttachment = (WriteAttachment) attachment;
+      if (keyAttachment.getWriteTasks().offer(writeTask)) {
+        iterator.remove();
+        try {
+          key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
+        } catch (CancelledKeyException e) {
+          writeTask.getListener().onWriteFailed(getDefaultWriteErrorMessageWithSuffix(socketChannel, "Key is cancelled"), new ConnectionClosedException(e));
+        }
+      }
+    }
+  }
+
+  private String getDefaultWriteErrorMessageWithSuffix(SocketChannel socketChannel, String suffix) {
+    return "unable write data to channel " + socketChannel + ". " + suffix;
+  }
+
+  private void connectToPeersFromQueue() {
+    ConnectTask connectTask;
+    while ((connectTask = myConnectQueue.poll()) != null) {
+      if (stop || Thread.currentThread().isInterrupted()) {
+        return;
+      }
+      logger.debug("try connect to peer. Connect task is {}", connectTask);
+      try {
+        SocketChannel socketChannel = SocketChannel.open();
+        socketChannel.configureBlocking(false);
+        socketChannel.register(selector, SelectionKey.OP_CONNECT, connectTask);
+        socketChannel.connect(new InetSocketAddress(connectTask.getHost(), connectTask.getPort()));
+      } catch (IOException e) {
+        LoggerUtils.warnAndDebugDetails(logger, "unable connect. Connect task is {}", connectTask, e);
+      }
+    }
+  }
+
+  public boolean stop(int timeout, TimeUnit timeUnit) throws InterruptedException {
+    stop = true;
+    if (timeout <= 0) {
+      return true;
+    }
+    return mySemaphore.tryAcquire(timeout, timeUnit);
+  }
+
+  private void processSelectedKeys() {
+    Set<SelectionKey> selectionKeys = selector.selectedKeys();
+    for (SelectionKey key : selectionKeys) {
+      if (stop || Thread.currentThread().isInterrupted()) {
+        return;
+      }
+      try {
+        processSelectedKey(key);
+      } catch (Exception e) {
+        logger.warn("error {} in processing key. Close channel {}", e.getMessage(), key.channel());
+        logger.debug("", e);
+        try {
+          key.channel().close();
+        } catch (IOException ioe) {
+          LoggerUtils.errorAndDebugDetails(logger, "unable close bad channel", ioe);
+        }
+      }
+    }
+    selectionKeys.clear();
+  }
+
+  private void processSelectedKey(SelectionKey key) throws IOException {
+    logger.trace("try process key for channel {}", key.channel());
+    myCleanupProcessor.processSelected(key);
+    if (!key.channel().isOpen()) {
+      key.cancel();
+      return;
+    }
+    for (KeyProcessor keyProcessor : myKeyProcessors) {
+      if (keyProcessor.accept(key)) {
+        keyProcessor.process(key);
+      }
+    }
+  }
+
+  public boolean offerConnect(ConnectTask connectTask, int timeout, TimeUnit timeUnit) {
+    if (!myNewConnectionAllower.isNewConnectionAllowed()) {
+      logger.info("can not add connect task {} to queue. New connection is not allowed", connectTask);
+      return false;
+    }
+    return addTaskToQueue(connectTask, timeout, timeUnit, myConnectQueue);
+  }
+
+  public boolean offerWrite(WriteTask writeTask, int timeout, TimeUnit timeUnit) {
+    boolean done = addTaskToQueue(writeTask, timeout, timeUnit, myWriteQueue);
+    if (!done) {
+      writeTask.getListener().onWriteFailed("unable add task " + writeTask + " to the queue. Maybe queue is overload", null);
+    }
+    return done;
+  }
+
+  private <T> boolean addTaskToQueue(T task, int timeout, TimeUnit timeUnit, BlockingQueue<T> queue) {
+    try {
+      if (queue.offer(task, timeout, timeUnit)) {
+        logger.trace("added task {}. Wake up selector", task);
+        selector.wakeup();
+        return true;
+      }
+    } catch (InterruptedException e) {
+      logger.debug("Task {} interrupted before was added to queue", task);
+    }
+    logger.debug("Task {} was not added", task);
+    return false;
+  }
+
+  void setCleanupTimeout(long timeoutMillis) {
+    this.myCleanupTimeoutMillis = timeoutMillis;
+  }
+
+  void setSelectorSelectTimeout(int timeout) {
+    mySelectorTimeoutMillis = timeout;
+  }
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/FirstAvailableChannel.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/FirstAvailableChannel.java
new file mode 100644
index 0000000..b31a7ef
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/FirstAvailableChannel.java
@@ -0,0 +1,49 @@
+package com.turn.ttorrent.network;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+
+public class FirstAvailableChannel implements ServerChannelRegister {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(FirstAvailableChannel.class);
+
+  private final int firstTryPort;
+  private final int lastTryPort;
+
+  public FirstAvailableChannel(int firstTryPort, int lastTryPort) {
+    this.firstTryPort = firstTryPort;
+    this.lastTryPort = lastTryPort;
+  }
+
+  @NotNull
+  @Override
+  public ServerSocketChannel channelFor(Selector selector) throws IOException {
+    ServerSocketChannel myServerSocketChannel = selector.provider().openServerSocketChannel();
+    myServerSocketChannel.configureBlocking(false);
+    int bindPort = -1;
+    for (int port = firstTryPort; port <= lastTryPort; port++) {
+      try {
+        InetSocketAddress tryAddress = new InetSocketAddress(port);
+        myServerSocketChannel.socket().bind(tryAddress);
+        bindPort = tryAddress.getPort();
+        break;
+      } catch (IOException e) {
+        //try next port
+        logger.debug("Could not bind to port {}, trying next port...", port);
+      }
+    }
+    if (bindPort == -1) {
+      logger.error(String.format(
+              "No available ports in range [%d, %d] for the BitTorrent client!", firstTryPort, lastTryPort
+      ));
+      throw new IOException("No available port for the BitTorrent client!");
+    }
+    return myServerSocketChannel;
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/NewConnectionAllower.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/NewConnectionAllower.java
new file mode 100644
index 0000000..c1f834f
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/NewConnectionAllower.java
@@ -0,0 +1,10 @@
+package com.turn.ttorrent.network;
+
+public interface NewConnectionAllower {
+
+  /**
+   * @return true if we can accept new connection or can connect to other peer
+   */
+  boolean isNewConnectionAllowed();
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ReadAttachment.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ReadAttachment.java
new file mode 100644
index 0000000..2072e30
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ReadAttachment.java
@@ -0,0 +1,9 @@
+package com.turn.ttorrent.network;
+
+public interface ReadAttachment {
+
+  /**
+   * @return connection listener, associated with key with current attachment
+   */
+  ConnectionListener getConnectionListener();
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ReadWriteAttachment.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ReadWriteAttachment.java
new file mode 100644
index 0000000..6bb06e4
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ReadWriteAttachment.java
@@ -0,0 +1,50 @@
+package com.turn.ttorrent.network;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public class ReadWriteAttachment implements ReadAttachment, WriteAttachment, TimeoutAttachment {
+
+  private final static int WRITE_TASK_QUEUE_SIZE = 150;
+
+  private long lastCommunicationTime;
+  private final ConnectionListener connectionListener;
+  private final long myTimeoutMillis;
+  private final BlockingQueue<WriteTask> writeTasks;
+
+  public ReadWriteAttachment(ConnectionListener connectionListener, long lastCommunicationTime, long timeoutMillis) {
+    this.connectionListener = connectionListener;
+    this.writeTasks = new LinkedBlockingQueue<WriteTask>(WRITE_TASK_QUEUE_SIZE);
+    this.lastCommunicationTime = lastCommunicationTime;
+    this.myTimeoutMillis = timeoutMillis;
+  }
+
+  @Override
+  public ConnectionListener getConnectionListener() {
+    return connectionListener;
+  }
+
+  @Override
+  public BlockingQueue<WriteTask> getWriteTasks() {
+    return writeTasks;
+  }
+
+  @Override
+  public boolean isTimeoutElapsed(long currentTimeMillis) {
+    long minTimeForKeepAlive = currentTimeMillis - myTimeoutMillis;
+    return minTimeForKeepAlive > lastCommunicationTime;
+  }
+
+  @Override
+  public void communicatedNow(long currentTimeMillis) {
+    lastCommunicationTime = currentTimeMillis;
+  }
+
+  @Override
+  public void onTimeoutElapsed(SocketChannel channel) throws IOException {
+    connectionListener.onError(channel, new SocketTimeoutException());
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/SelectorFactory.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/SelectorFactory.java
new file mode 100644
index 0000000..b3b99b2
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/SelectorFactory.java
@@ -0,0 +1,30 @@
+/*
+ * 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.network;
+
+import java.io.IOException;
+import java.nio.channels.Selector;
+
+public interface SelectorFactory {
+
+  /**
+   * @return new {@link Selector} instance
+   * @throws IOException if any io error occurs
+   */
+  Selector newSelector() throws IOException;
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ServerChannelRegister.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ServerChannelRegister.java
new file mode 100644
index 0000000..8f69c5d
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/ServerChannelRegister.java
@@ -0,0 +1,20 @@
+package com.turn.ttorrent.network;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+
+public interface ServerChannelRegister {
+
+  /**
+   * Create new channel and bind to specified selector
+   *
+   * @param selector specified selector
+   * @return new created server channel
+   */
+  @NotNull
+  ServerSocketChannel channelFor(Selector selector) throws IOException;
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutAttachment.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutAttachment.java
new file mode 100644
index 0000000..6478666
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutAttachment.java
@@ -0,0 +1,29 @@
+package com.turn.ttorrent.network;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+
+public interface TimeoutAttachment {
+
+  /**
+   * @param currentTimeMillis current time for timeout calculation
+   * @return true, if and only if timeout was elapsed
+   */
+  boolean isTimeoutElapsed(long currentTimeMillis);
+
+  /**
+   * set last communication time to current time
+   *
+   * @param currentTimeMillis current time in milliseconds
+   */
+  void communicatedNow(long currentTimeMillis);
+
+  /**
+   * must be invoked if timeout was elapsed
+   *
+   * @param channel specified channel for key associated with this attachment
+   * @throws IOException if an I/O error occurs
+   */
+  void onTimeoutElapsed(SocketChannel channel) throws IOException;
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutStorage.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutStorage.java
new file mode 100644
index 0000000..ff14d92
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutStorage.java
@@ -0,0 +1,13 @@
+package com.turn.ttorrent.network;
+
+import java.util.concurrent.TimeUnit;
+
+public interface TimeoutStorage {
+
+  void setTimeout(long millis);
+
+  void setTimeout(int timeout, TimeUnit timeUnit);
+
+  long getTimeoutMillis();
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutStorageImpl.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutStorageImpl.java
new file mode 100644
index 0000000..529f0da
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/TimeoutStorageImpl.java
@@ -0,0 +1,24 @@
+package com.turn.ttorrent.network;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class TimeoutStorageImpl implements TimeoutStorage {
+
+  private final AtomicLong timeoutMillis = new AtomicLong();
+
+  @Override
+  public void setTimeout(long millis) {
+    timeoutMillis.set(millis);
+  }
+
+  @Override
+  public void setTimeout(int timeout, TimeUnit timeUnit) {
+    setTimeout(timeUnit.toMillis(timeout));
+  }
+
+  @Override
+  public long getTimeoutMillis() {
+    return timeoutMillis.get();
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteAttachment.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteAttachment.java
new file mode 100644
index 0000000..3497279
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteAttachment.java
@@ -0,0 +1,12 @@
+package com.turn.ttorrent.network;
+
+import java.util.concurrent.BlockingQueue;
+
+public interface WriteAttachment {
+
+  /**
+   * @return queue for offer/peek write tasks
+   */
+  BlockingQueue<WriteTask> getWriteTasks();
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteListener.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteListener.java
new file mode 100644
index 0000000..4743e53
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteListener.java
@@ -0,0 +1,18 @@
+package com.turn.ttorrent.network;
+
+public interface WriteListener {
+
+  /**
+   * invoked if write is failed by any reason
+   *
+   * @param message error description
+   * @param e       exception if exist. Otherwise null
+   */
+  void onWriteFailed(String message, Throwable e);
+
+  /**
+   * invoked if write done correctly
+   */
+  void onWriteDone();
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteTask.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteTask.java
new file mode 100644
index 0000000..838752b
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/WriteTask.java
@@ -0,0 +1,38 @@
+package com.turn.ttorrent.network;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+
+public class WriteTask {
+
+  private final ByteChannel socketChannel;
+  private final ByteBuffer byteBuffer;
+  private final WriteListener listener;
+
+  public WriteTask(ByteChannel socketChannel, ByteBuffer byteBuffer, WriteListener listener) {
+    this.socketChannel = socketChannel;
+    this.byteBuffer = byteBuffer;
+    this.listener = listener;
+  }
+
+  public ByteChannel getSocketChannel() {
+    return socketChannel;
+  }
+
+  public ByteBuffer getByteBuffer() {
+    return byteBuffer;
+  }
+
+  public WriteListener getListener() {
+    return listener;
+  }
+
+  @Override
+  public String toString() {
+    return "WriteTask{" +
+            "socketChannel=" + socketChannel +
+            ", byteBuffer=" + byteBuffer +
+            ", listener=" + listener +
+            '}';
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/AcceptableKeyProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/AcceptableKeyProcessor.java
new file mode 100644
index 0000000..e3b07c2
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/AcceptableKeyProcessor.java
@@ -0,0 +1,77 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.common.TimeService;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.*;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class AcceptableKeyProcessor implements KeyProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(AcceptableKeyProcessor.class);
+
+  private final Selector mySelector;
+  private final String myServerSocketLocalAddress;
+  private final TimeService myTimeService;
+  private final NewConnectionAllower myNewConnectionAllower;
+  private final TimeoutStorage myTimeoutStorage;
+  private final AtomicInteger mySendBufferSize;
+  private final AtomicInteger myReceiveBufferSize;
+
+  public AcceptableKeyProcessor(Selector selector,
+                                String serverSocketLocalAddress,
+                                TimeService timeService,
+                                NewConnectionAllower newConnectionAllower,
+                                TimeoutStorage timeoutStorage,
+                                AtomicInteger sendBufferSize,
+                                AtomicInteger receiveBufferSize) {
+    this.mySelector = selector;
+    this.myServerSocketLocalAddress = serverSocketLocalAddress;
+    this.myTimeService = timeService;
+    this.myNewConnectionAllower = newConnectionAllower;
+    this.myTimeoutStorage = timeoutStorage;
+    this.mySendBufferSize = sendBufferSize;
+    this.myReceiveBufferSize = receiveBufferSize;
+  }
+
+  @Override
+  public void process(SelectionKey key) throws IOException {
+    SelectableChannel channel = key.channel();
+    if (!(channel instanceof ServerSocketChannel)) {
+      logger.error("incorrect instance of server channel. Can not accept connections");
+      key.cancel();
+      return;
+    }
+    Object attachment = key.attachment();
+    if (!(attachment instanceof AcceptAttachment)) {
+      logger.error("incorrect instance of server channel key attachment");
+      key.cancel();
+      return;
+    }
+    ChannelListenerFactory channelListenerFactory = ((AcceptAttachment) attachment).getChannelListenerFactory();
+
+    SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();
+    logger.trace("server {} get new connection from {}", new Object[]{myServerSocketLocalAddress, socketChannel.socket()});
+
+    if (!myNewConnectionAllower.isNewConnectionAllowed()) {
+      logger.info("new connection is not allowed. New connection is closed");
+      socketChannel.close();
+      return;
+    }
+
+    ConnectionListener stateConnectionListener = channelListenerFactory.newChannelListener();
+    stateConnectionListener.onConnectionEstablished(socketChannel);
+    socketChannel.configureBlocking(false);
+    KeyProcessorUtil.setBuffersSizeIfNecessary(socketChannel, mySendBufferSize.get(), myReceiveBufferSize.get());
+    ReadWriteAttachment keyAttachment = new ReadWriteAttachment(stateConnectionListener, myTimeService.now(), myTimeoutStorage.getTimeoutMillis());
+    socketChannel.register(mySelector, SelectionKey.OP_READ, keyAttachment);
+  }
+
+  @Override
+  public boolean accept(SelectionKey key) {
+    return key.isValid() && key.isAcceptable();
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/CleanupKeyProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/CleanupKeyProcessor.java
new file mode 100644
index 0000000..34e7f78
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/CleanupKeyProcessor.java
@@ -0,0 +1,58 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.TimeService;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.TimeoutAttachment;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+public class CleanupKeyProcessor implements CleanupProcessor {
+
+  private final static Logger logger = TorrentLoggerFactory.getLogger(CleanupKeyProcessor.class);
+
+  private final TimeService myTimeService;
+
+  public CleanupKeyProcessor(TimeService timeService) {
+    this.myTimeService = timeService;
+  }
+
+  @Override
+  public void processCleanup(SelectionKey key) {
+    TimeoutAttachment attachment = KeyProcessorUtil.getAttachmentAsTimeoutOrNull(key);
+    if (attachment == null) {
+      key.cancel();
+      return;
+    }
+    if (attachment.isTimeoutElapsed(myTimeService.now())) {
+
+      SocketChannel channel = KeyProcessorUtil.getCastedChannelOrNull(key);
+      if (channel == null) {
+        key.cancel();
+        return;
+      }
+
+      logger.debug("channel {} was inactive in specified timeout. Close channel...", channel);
+      try {
+        channel.close();
+        key.cancel();
+        attachment.onTimeoutElapsed(channel);
+      } catch (IOException e) {
+        LoggerUtils.errorAndDebugDetails(logger, "unable close channel {}", channel, e);
+      }
+    }
+  }
+
+  @Override
+  public void processSelected(SelectionKey key) {
+    TimeoutAttachment attachment = KeyProcessorUtil.getAttachmentAsTimeoutOrNull(key);
+    if (attachment == null) {
+      key.cancel();
+      return;
+    }
+    attachment.communicatedNow(myTimeService.now());
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/CleanupProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/CleanupProcessor.java
new file mode 100644
index 0000000..c3d50c7
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/CleanupProcessor.java
@@ -0,0 +1,21 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import java.nio.channels.SelectionKey;
+
+public interface CleanupProcessor {
+
+  /**
+   * invoked when the cleanup procedure is running. Processor can cancel key and/or close channel if necessary
+   *
+   * @param key specified key
+   */
+  void processCleanup(SelectionKey key);
+
+  /**
+   * invoked when get any activity for channel associated with this key
+   *
+   * @param key specified key
+   */
+  void processSelected(SelectionKey key);
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/ConnectableKeyProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/ConnectableKeyProcessor.java
new file mode 100644
index 0000000..6b6474a
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/ConnectableKeyProcessor.java
@@ -0,0 +1,88 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.common.TimeService;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.ConnectTask;
+import com.turn.ttorrent.network.ConnectionListener;
+import com.turn.ttorrent.network.ReadWriteAttachment;
+import com.turn.ttorrent.network.TimeoutStorage;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.NoRouteToHostException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ConnectableKeyProcessor implements KeyProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(ConnectableKeyProcessor.class);
+
+  private final Selector mySelector;
+  private final TimeService myTimeService;
+  private final TimeoutStorage myTimeoutStorage;
+  private final AtomicInteger mySendBufferSize;
+  private final AtomicInteger myReceiveBufferSize;
+
+  public ConnectableKeyProcessor(Selector selector,
+                                 TimeService timeService,
+                                 TimeoutStorage timeoutStorage,
+                                 AtomicInteger sendBufferSize,
+                                 AtomicInteger receiveBufferSize) {
+    this.mySelector = selector;
+    this.myTimeService = timeService;
+    this.myTimeoutStorage = timeoutStorage;
+    this.mySendBufferSize = sendBufferSize;
+    this.myReceiveBufferSize = receiveBufferSize;
+  }
+
+  @Override
+  public void process(SelectionKey key) throws IOException {
+    SelectableChannel channel = key.channel();
+    if (!(channel instanceof SocketChannel)) {
+      logger.warn("incorrect instance of channel. The key is cancelled");
+      key.cancel();
+      return;
+    }
+    SocketChannel socketChannel = (SocketChannel) channel;
+    Object attachment = key.attachment();
+    if (!(attachment instanceof ConnectTask)) {
+      logger.warn("incorrect instance of attachment for channel {}. The key for the channel is cancelled", socketChannel);
+      key.cancel();
+      return;
+    }
+    final ConnectTask connectTask = (ConnectTask) attachment;
+    final ConnectionListener connectionListener = connectTask.getConnectionListener();
+    final boolean isConnectFinished;
+    try {
+      isConnectFinished = socketChannel.finishConnect();
+    } catch (NoRouteToHostException e) {
+      logger.info("Could not connect to {}:{}, received NoRouteToHostException", connectTask.getHost(), connectTask.getPort());
+      connectionListener.onError(socketChannel, e);
+      return;
+    } catch (ConnectException e) {
+      logger.info("Could not connect to {}:{}, received ConnectException", connectTask.getHost(), connectTask.getPort());
+      connectionListener.onError(socketChannel, e);
+      return;
+    }
+    if (!isConnectFinished) {
+      logger.info("Could not connect to {}:{}", connectTask.getHost(), connectTask.getPort());
+      connectionListener.onError(socketChannel, null);
+      return;
+    }
+    socketChannel.configureBlocking(false);
+    KeyProcessorUtil.setBuffersSizeIfNecessary(socketChannel, mySendBufferSize.get(), myReceiveBufferSize.get());
+    ReadWriteAttachment keyAttachment = new ReadWriteAttachment(connectionListener, myTimeService.now(), myTimeoutStorage.getTimeoutMillis());
+    socketChannel.register(mySelector, SelectionKey.OP_READ, keyAttachment);
+    logger.debug("setup new TCP connection with {}", socketChannel);
+    connectionListener.onConnectionEstablished(socketChannel);
+  }
+
+  @Override
+  public boolean accept(SelectionKey key) {
+    return key.isValid() && key.isConnectable();
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/InvalidKeyProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/InvalidKeyProcessor.java
new file mode 100644
index 0000000..259f48f
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/InvalidKeyProcessor.java
@@ -0,0 +1,44 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.ReadAttachment;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+public class InvalidKeyProcessor implements KeyProcessor {
+
+  private final static Logger logger = TorrentLoggerFactory.getLogger(InvalidKeyProcessor.class);
+
+  @Override
+  public void process(SelectionKey key) throws IOException {
+    final Object attachment = key.attachment();
+    final SelectableChannel channel = key.channel();
+    if (attachment == null) {
+      key.cancel();
+      return;
+    }
+    if (!(attachment instanceof ReadAttachment)) {
+      key.cancel();
+      return;
+    }
+    if (!(channel instanceof SocketChannel)) {
+      key.cancel();
+      return;
+    }
+    final SocketChannel socketChannel = (SocketChannel) channel;
+    final ReadAttachment readAttachment = (ReadAttachment) attachment;
+
+    logger.trace("drop invalid key {}", channel);
+    readAttachment.getConnectionListener().onError(socketChannel, new CancelledKeyException());
+  }
+
+  @Override
+  public boolean accept(SelectionKey key) {
+    return !key.isValid();
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/KeyProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/KeyProcessor.java
new file mode 100644
index 0000000..2a8f51e
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/KeyProcessor.java
@@ -0,0 +1,22 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import java.io.IOException;
+import java.nio.channels.SelectionKey;
+
+public interface KeyProcessor {
+
+  /**
+   * processes the passed key
+   *
+   * @param key key for processing
+   * @throws IOException if an I/O error occurs
+   */
+  void process(SelectionKey key) throws IOException;
+
+  /**
+   * @param key specified key for check acceptance
+   * @return true if and only if processor can process this key.
+   */
+  boolean accept(SelectionKey key);
+
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/KeyProcessorUtil.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/KeyProcessorUtil.java
new file mode 100644
index 0000000..c097942
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/KeyProcessorUtil.java
@@ -0,0 +1,44 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.TimeoutAttachment;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+public class KeyProcessorUtil {
+
+  private final static Logger logger = TorrentLoggerFactory.getLogger(KeyProcessorUtil.class);
+
+  public static TimeoutAttachment getAttachmentAsTimeoutOrNull(SelectionKey key) {
+    Object attachment = key.attachment();
+    if (attachment instanceof TimeoutAttachment) {
+      return (TimeoutAttachment) attachment;
+    }
+    logger.error("unable to cast attachment {} to timeout attachment type", attachment);
+    return null;
+  }
+
+  public static SocketChannel getCastedChannelOrNull(SelectionKey key) {
+    SelectableChannel channel = key.channel();
+    if (channel instanceof SocketChannel) {
+      return (SocketChannel) channel;
+    }
+    logger.error("unable to cast channel {} to specified type");
+    return null;
+  }
+
+  public static void setBuffersSizeIfNecessary(SocketChannel socketChannel, int sendBufferSize, int receiveBufferSize) throws IOException {
+    final Socket socket = socketChannel.socket();
+    if (sendBufferSize > 0) {
+      socket.setSendBufferSize(sendBufferSize);
+    }
+    if (receiveBufferSize > 0) {
+      socket.setReceiveBufferSize(receiveBufferSize);
+    }
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/ReadableKeyProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/ReadableKeyProcessor.java
new file mode 100644
index 0000000..d209919
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/ReadableKeyProcessor.java
@@ -0,0 +1,49 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.ConnectionListener;
+import com.turn.ttorrent.network.ReadAttachment;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+public class ReadableKeyProcessor implements KeyProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(ReadableKeyProcessor.class);
+
+  private final String myServerSocketLocalAddress;
+
+  public ReadableKeyProcessor(String serverSocketLocalAddress) {
+    this.myServerSocketLocalAddress = serverSocketLocalAddress;
+  }
+
+  @Override
+  public void process(SelectionKey key) throws IOException {
+    SelectableChannel channel = key.channel();
+    if (!(channel instanceof SocketChannel)) {
+      logger.warn("incorrect instance of channel. The key is cancelled");
+      key.cancel();
+      return;
+    }
+
+    SocketChannel socketChannel = (SocketChannel) channel;
+    logger.trace("server {} get new data from {}", myServerSocketLocalAddress, socketChannel);
+
+    Object attachment = key.attachment();
+    if (!(attachment instanceof ReadAttachment)) {
+      logger.warn("incorrect instance of attachment for channel {}", new Object[]{socketChannel.socket()});
+      socketChannel.close();
+      return;
+    }
+    ConnectionListener connectionListener = ((ReadAttachment) attachment).getConnectionListener();
+    connectionListener.onNewDataAvailable(socketChannel);
+  }
+
+  @Override
+  public boolean accept(SelectionKey key) {
+    return key.isValid() && key.isReadable();
+  }
+}
diff --git a/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/WritableKeyProcessor.java b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/WritableKeyProcessor.java
new file mode 100644
index 0000000..dea5be2
--- /dev/null
+++ b/ttorrent-master/network/src/main/java/com/turn/ttorrent/network/keyProcessors/WritableKeyProcessor.java
@@ -0,0 +1,69 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.network.ConnectionClosedException;
+import com.turn.ttorrent.network.WriteAttachment;
+import com.turn.ttorrent.network.WriteTask;
+import org.slf4j.Logger;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+public class WritableKeyProcessor implements KeyProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(WritableKeyProcessor.class);
+
+  @Override
+  public void process(SelectionKey key) throws IOException {
+    SelectableChannel channel = key.channel();
+    if (!(channel instanceof SocketChannel)) {
+      logger.warn("incorrect instance of channel. The key is cancelled");
+      key.cancel();
+      return;
+    }
+
+    SocketChannel socketChannel = (SocketChannel) channel;
+
+    Object attachment = key.attachment();
+    if (!(attachment instanceof WriteAttachment)) {
+      logger.error("incorrect instance of attachment for channel {}", channel);
+      key.cancel();
+      return;
+    }
+
+    WriteAttachment keyAttachment = (WriteAttachment) attachment;
+
+    if (keyAttachment.getWriteTasks().isEmpty()) {
+      key.interestOps(SelectionKey.OP_READ);
+      return;
+    }
+
+    WriteTask processedTask = keyAttachment.getWriteTasks().peek();
+
+    try {
+      int writeCount = socketChannel.write(processedTask.getByteBuffer());
+      if (writeCount < 0) {
+        processedTask.getListener().onWriteFailed("Reached end of stream while writing", null);
+        throw new EOFException("Reached end of stream while writing");
+      }
+
+      if (!processedTask.getByteBuffer().hasRemaining()) {
+        processedTask.getListener().onWriteDone();
+        keyAttachment.getWriteTasks().remove();
+      }
+
+    } catch (IOException e) {
+      processedTask.getListener().onWriteFailed("I/O error occurs on write to channel " + socketChannel, new ConnectionClosedException(e));
+      keyAttachment.getWriteTasks().clear();
+      key.cancel();
+    }
+  }
+
+  @Override
+  public boolean accept(SelectionKey key) {
+    return key.isValid() && key.isWritable();
+  }
+}
diff --git a/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/ConnectionManagerTest.java b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/ConnectionManagerTest.java
new file mode 100644
index 0000000..e35ffe8
--- /dev/null
+++ b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/ConnectionManagerTest.java
@@ -0,0 +1,189 @@
+package com.turn.ttorrent.network;
+
+import com.turn.ttorrent.MockTimeService;
+import org.apache.log4j.*;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.*;
+
+public class ConnectionManagerTest {
+
+  private ConnectionManager myConnectionManager;
+  private ExecutorService myExecutorService;
+  private ConnectionListener connectionListener;
+  private ConnectionManagerContext myContext;
+
+  public ConnectionManagerTest() {
+    if (Logger.getRootLogger().getAllAppenders().hasMoreElements())
+      return;
+    BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("[%d{MMdd HH:mm:ss,SSS} %t] %6p - %20.20c - %m %n")));
+    Logger.getRootLogger().setLevel(Level.ALL);
+  }
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    Logger.getRootLogger().setLevel(Level.INFO);
+    myContext = mock(ConnectionManagerContext.class);
+    myExecutorService = Executors.newSingleThreadExecutor();
+    when(myContext.getExecutor()).thenReturn(myExecutorService);
+    final SelectorFactory selectorFactory = mock(SelectorFactory.class);
+    when(selectorFactory.newSelector()).thenReturn(Selector.open());
+    NewConnectionAllower newConnectionAllower = mock(NewConnectionAllower.class);
+    when(newConnectionAllower.isNewConnectionAllowed()).thenReturn(true);
+    myConnectionManager = new ConnectionManager(
+            myContext,
+            new MockTimeService(),
+            newConnectionAllower,
+            newConnectionAllower,
+            selectorFactory,
+            new AtomicInteger(),
+            new AtomicInteger());
+  }
+
+  @Test(expectedExceptions = IllegalStateException.class)
+  public void testThatDoubleInitThrowException() {
+    try {
+      myConnectionManager.initAndRunWorker(new FirstAvailableChannel(6881, 6889));
+    } catch (IOException e) {
+      fail("unable to init and run worker", e);
+    }
+    try {
+      myConnectionManager.initAndRunWorker(new FirstAvailableChannel(6881, 6889));
+    } catch (IOException e) {
+      fail("unable to init and run worker", e);
+    }
+  }
+
+  @Test
+  public void canAcceptAndReadData() throws IOException, InterruptedException {
+    final AtomicInteger acceptCount = new AtomicInteger();
+    final AtomicInteger readCount = new AtomicInteger();
+    final AtomicInteger connectCount = new AtomicInteger();
+    final AtomicInteger lastReadBytesCount = new AtomicInteger();
+    final ByteBuffer byteBuffer = ByteBuffer.allocate(10);
+
+    final Semaphore semaphore = new Semaphore(0);
+
+    this.connectionListener = new ConnectionListener() {
+      @Override
+      public void onNewDataAvailable(SocketChannel socketChannel) throws IOException {
+        readCount.incrementAndGet();
+        lastReadBytesCount.set(socketChannel.read(byteBuffer));
+        if (lastReadBytesCount.get() == -1) {
+          socketChannel.close();
+        }
+        semaphore.release();
+      }
+
+      @Override
+      public void onConnectionEstablished(SocketChannel socketChannel) throws IOException {
+        acceptCount.incrementAndGet();
+        semaphore.release();
+      }
+
+      @Override
+      public void onError(SocketChannel socketChannel, Throwable ex) {
+
+      }
+    };
+
+    when(myContext.newChannelListener()).thenReturn(connectionListener);
+
+    myConnectionManager.initAndRunWorker(new FirstAvailableChannel(6881, 6889));
+
+    assertEquals(acceptCount.get(), 0);
+    assertEquals(readCount.get(), 0);
+    int serverPort = myConnectionManager.getBindPort();
+
+    Socket socket = new Socket("127.0.0.1", serverPort);
+
+    tryAcquireOrFail(semaphore);//wait until connection is accepted
+
+    assertTrue(socket.isConnected());
+    assertEquals(acceptCount.get(), 1);
+    assertEquals(readCount.get(), 0);
+
+    Socket socketSecond = new Socket("127.0.0.1", serverPort);
+
+    tryAcquireOrFail(semaphore);//wait until connection is accepted
+
+    assertTrue(socketSecond.isConnected());
+    assertEquals(acceptCount.get(), 2);
+    assertEquals(readCount.get(), 0);
+    socketSecond.close();
+    tryAcquireOrFail(semaphore);//wait read that connection is closed
+    assertEquals(readCount.get(), 1);
+    assertEquals(acceptCount.get(), 2);
+    assertEquals(lastReadBytesCount.get(), -1);
+    byteBuffer.rewind();
+    assertEquals(byteBuffer.get(), 0);
+    byteBuffer.rewind();
+    String writeStr = "abc";
+    OutputStream outputStream = socket.getOutputStream();
+    outputStream.write(writeStr.getBytes());
+    tryAcquireOrFail(semaphore);//wait until read bytes
+    assertEquals(readCount.get(), 2);
+    assertEquals(lastReadBytesCount.get(), 3);
+    byte[] expected = new byte[byteBuffer.capacity()];
+    System.arraycopy(writeStr.getBytes(), 0, expected, 0, writeStr.length());
+    assertEquals(byteBuffer.array(), expected);
+    outputStream.close();
+    socket.close();
+    tryAcquireOrFail(semaphore);//wait read that connection is closed
+    assertEquals(readCount.get(), 3);
+
+    int otherPeerPort = 7575;
+    ServerSocket ss = new ServerSocket(otherPeerPort);
+    assertEquals(connectCount.get(), 0);
+    myConnectionManager.offerConnect(new ConnectTask("127.0.0.1", otherPeerPort, new ConnectionListener() {
+      @Override
+      public void onNewDataAvailable(SocketChannel socketChannel) throws IOException {
+
+      }
+
+      @Override
+      public void onConnectionEstablished(SocketChannel socketChannel) throws IOException {
+        connectCount.incrementAndGet();
+        semaphore.release();
+      }
+
+      @Override
+      public void onError(SocketChannel socketChannel, Throwable ex) {
+
+      }
+    }, 0, 100), 1, TimeUnit.SECONDS);
+    ss.accept();
+    tryAcquireOrFail(semaphore);
+    assertEquals(connectCount.get(), 1);
+  }
+
+  @AfterMethod
+  public void tearDown() throws Exception {
+    this.myConnectionManager.close();
+    myExecutorService.shutdown();
+    assertTrue(myExecutorService.awaitTermination(10, TimeUnit.SECONDS));
+  }
+
+  private void tryAcquireOrFail(Semaphore semaphore) throws InterruptedException {
+    if (!semaphore.tryAcquire(500, TimeUnit.MILLISECONDS)) {
+      fail("don't get signal from connection receiver that connection selected");
+    }
+  }
+}
diff --git a/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/ConnectionWorkerTest.java b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/ConnectionWorkerTest.java
new file mode 100644
index 0000000..237d163
--- /dev/null
+++ b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/ConnectionWorkerTest.java
@@ -0,0 +1,48 @@
+package com.turn.ttorrent.network;
+
+import com.turn.ttorrent.MockTimeService;
+import com.turn.ttorrent.network.keyProcessors.CleanupProcessor;
+import com.turn.ttorrent.network.keyProcessors.KeyProcessor;
+import org.testng.annotations.Test;
+
+import java.nio.channels.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import static org.mockito.Mockito.*;
+
+@Test
+public class ConnectionWorkerTest {
+
+  public void testCleanupIsCalled() throws Exception {
+
+    final SelectionKey mockKey = mock(SelectionKey.class);
+    final SelectableChannel channel = SocketChannel.open();
+    final KeyProcessor acceptProcessor = mock(KeyProcessor.class);
+    final KeyProcessor notAcceptProcessor = mock(KeyProcessor.class);
+
+    Selector mockSelector = mock(Selector.class);
+    when(mockSelector.select(anyLong())).thenReturn(1).thenThrow(new ClosedSelectorException());
+    when(mockSelector.selectedKeys()).thenReturn(new HashSet<SelectionKey>(Collections.singleton(mockKey)));
+    when(mockKey.isValid()).thenReturn(true);
+    when(mockKey.channel()).thenReturn(channel);
+    when(acceptProcessor.accept(mockKey)).thenReturn(true);
+    when(notAcceptProcessor.accept(mockKey)).thenReturn(false);
+    ConnectionWorker connectionWorker = new ConnectionWorker(
+            mockSelector,
+            Arrays.asList(acceptProcessor, notAcceptProcessor),
+            10,
+            0,
+            new MockTimeService(),
+            mock(CleanupProcessor.class),
+            mock(NewConnectionAllower.class));
+    connectionWorker.run();
+    verify(mockSelector).selectedKeys();
+    verify(acceptProcessor).accept(mockKey);
+    verify(acceptProcessor).process(mockKey);
+    verify(notAcceptProcessor).accept(mockKey);
+    verifyNoMoreInteractions(notAcceptProcessor);
+  }
+}
+
diff --git a/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/StateChannelListenerTest.java b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/StateChannelListenerTest.java
new file mode 100644
index 0000000..5c38fac
--- /dev/null
+++ b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/StateChannelListenerTest.java
@@ -0,0 +1,4 @@
+package com.turn.ttorrent.network;
+
+public class StateChannelListenerTest {
+}
diff --git a/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/WorkingReceiverTest.java b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/WorkingReceiverTest.java
new file mode 100644
index 0000000..434d17a
--- /dev/null
+++ b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/WorkingReceiverTest.java
@@ -0,0 +1,4 @@
+package com.turn.ttorrent.network;
+
+public class WorkingReceiverTest {
+}
diff --git a/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/keyProcessors/CleanupKeyProcessorTest.java b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/keyProcessors/CleanupKeyProcessorTest.java
new file mode 100644
index 0000000..3797d30
--- /dev/null
+++ b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/keyProcessors/CleanupKeyProcessorTest.java
@@ -0,0 +1,86 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.MockTimeService;
+import com.turn.ttorrent.network.ConnectionListener;
+import com.turn.ttorrent.network.TimeoutAttachment;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+import static org.mockito.Mockito.*;
+
+@Test
+public class CleanupKeyProcessorTest {
+
+  private final int CLOSE_TIMEOUT = 100;
+
+  private MockTimeService myTimeService;
+  private TimeoutAttachment myTimeoutAttachment;
+  private SelectionKey myKey;
+  private SelectableChannel myChannel;
+  private ConnectionListener myConnectionListener;
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myTimeService = new MockTimeService();
+    myConnectionListener = mock(ConnectionListener.class);
+    myTimeoutAttachment = mock(TimeoutAttachment.class);
+    myKey = mock(SelectionKey.class);
+    myChannel = SocketChannel.open();
+    when(myKey.channel()).thenReturn(myChannel);
+    when(myKey.interestOps()).thenReturn(SelectionKey.OP_READ);
+    myKey.attach(myTimeoutAttachment);
+  }
+
+  public void testSelected() {
+
+    long oldTime = 10;
+    myTimeService.setTime(oldTime);
+
+    CleanupProcessor cleanupProcessor = new CleanupKeyProcessor(myTimeService);
+    cleanupProcessor.processSelected(myKey);
+
+    verify(myTimeoutAttachment).communicatedNow(eq(oldTime));
+
+    long newTime = 100;
+    myTimeService.setTime(newTime);
+
+    cleanupProcessor.processSelected(myKey);
+
+    verify(myTimeoutAttachment).communicatedNow(eq(newTime));
+  }
+
+  public void testCleanupWillCloseWithTimeout() throws Exception {
+
+    when(myTimeoutAttachment.isTimeoutElapsed(anyLong())).thenReturn(true);
+
+    CleanupProcessor cleanupProcessor = new CleanupKeyProcessor(myTimeService);
+    cleanupProcessor.processCleanup(myKey);
+
+    verify(myKey).cancel();
+    verify(myKey).channel();
+    verify(myTimeoutAttachment).onTimeoutElapsed(any(SocketChannel.class));
+    verifyNoMoreInteractions(myKey);
+  }
+
+  public void testCleanupWithoutClose() {
+    when(myTimeoutAttachment.isTimeoutElapsed(anyLong())).thenReturn(false);
+
+    myTimeService.setTime(200);
+
+    CleanupProcessor cleanupProcessor = new CleanupKeyProcessor(myTimeService);
+    cleanupProcessor.processCleanup(myKey);
+
+    verify(myTimeoutAttachment).isTimeoutElapsed(myTimeService.now());
+    verify(myKey, never()).cancel();
+  }
+
+  @AfterMethod
+  public void tearDown() throws Exception {
+    myChannel.close();
+  }
+}
\ No newline at end of file
diff --git a/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/keyProcessors/WritableKeyProcessorTest.java b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/keyProcessors/WritableKeyProcessorTest.java
new file mode 100644
index 0000000..b784952
--- /dev/null
+++ b/ttorrent-master/network/src/test/java/com/turn/ttorrent/network/keyProcessors/WritableKeyProcessorTest.java
@@ -0,0 +1,102 @@
+package com.turn.ttorrent.network.keyProcessors;
+
+import com.turn.ttorrent.network.WriteAttachment;
+import com.turn.ttorrent.network.WriteListener;
+import com.turn.ttorrent.network.WriteTask;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.BlockingQueue;
+
+import static org.mockito.Mockito.*;
+
+@Test
+public class WritableKeyProcessorTest {
+
+  private SelectionKey myKey;
+  private SocketChannel myChannel;
+  private WritableKeyProcessor myWritableKeyProcessor;
+  private WriteAttachment myWriteAttachment;
+  private BlockingQueue<WriteTask> myQueue;
+
+
+  @SuppressWarnings("unchecked")
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myKey = mock(SelectionKey.class);
+    myChannel = mock(SocketChannel.class);
+    myWritableKeyProcessor = new WritableKeyProcessor();
+    when(myKey.channel()).thenReturn(myChannel);
+    when(myKey.interestOps()).thenReturn(SelectionKey.OP_WRITE);
+    myWriteAttachment = mock(WriteAttachment.class);
+    myQueue = mock(BlockingQueue.class);
+  }
+
+  public void testThatOnWriteDoneInvoked() throws Exception {
+    final ByteBuffer data = ByteBuffer.allocate(10);
+
+    //imitate writing byte buffer
+    when(myChannel.write(eq(data))).then(new Answer<Integer>() {
+      @Override
+      public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
+        data.position(data.capacity());
+        return data.capacity();
+      }
+    });
+
+    WriteListener listener = mock(WriteListener.class);
+
+    when(myQueue.peek()).thenReturn(new WriteTask(myChannel, data, listener));
+    when(myWriteAttachment.getWriteTasks()).thenReturn(myQueue);
+
+    myKey.attach(myWriteAttachment);
+
+    myWritableKeyProcessor.process(myKey);
+
+    verify(listener).onWriteDone();
+  }
+
+  public void testThatOnWriteFailedInvokedIfChannelThrowException() throws Exception {
+    when(myChannel.write(any(ByteBuffer.class))).thenThrow(new IOException());
+
+    WriteListener listener = mock(WriteListener.class);
+
+    when(myQueue.peek()).thenReturn(new WriteTask(myChannel, ByteBuffer.allocate(1), listener));
+    when(myWriteAttachment.getWriteTasks()).thenReturn(myQueue);
+    myKey.attach(myWriteAttachment);
+
+    myWritableKeyProcessor.process(myKey);
+
+    verify(listener).onWriteFailed(anyString(), any(Throwable.class));
+  }
+
+  public void checkThatWriteTaskDoesntRemovedIfBufferIsNotWrittenInOneStep() throws Exception {
+    final ByteBuffer data = ByteBuffer.allocate(10);
+
+    //imitate writing only one byte of byte buffer
+    when(myChannel.write(eq(data))).then(new Answer<Integer>() {
+      @Override
+      public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
+        data.position(data.capacity() - 1);
+        return data.position();
+      }
+    });
+
+    WriteListener listener = mock(WriteListener.class);
+
+    when(myQueue.peek()).thenReturn(new WriteTask(myChannel, data, listener));
+    when(myWriteAttachment.getWriteTasks()).thenReturn(myQueue);
+
+    myKey.attach(myWriteAttachment);
+
+    myWritableKeyProcessor.process(myKey);
+
+    verify(listener, never()).onWriteDone();
+  }
+}
diff --git a/ttorrent-master/pom.xml b/ttorrent-master/pom.xml
new file mode 100644
index 0000000..f801b33
--- /dev/null
+++ b/ttorrent-master/pom.xml
@@ -0,0 +1,227 @@
+<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>org.sonatype.oss</groupId>
+        <artifactId>oss-parent</artifactId>
+        <version>7</version>
+    </parent>
+
+    <name>Java BitTorrent library</name>
+    <description>
+        ttorrent is a pure-Java implementation of the BitTorrent protocol,
+        including support for several BEPs. It also provides a standalone client,
+        a tracker and a torrent manipulation utility.
+<!--        ttorrent 是 BitTorrent 协议的纯 Java 实现、包括对多个 BEP 的支持。它还提供了一个独立客户端、跟踪器和 torrent 操作工具。-->
+    </description>
+    <url>http://turn.github.com/ttorrent/</url>
+    <groupId>com.turn</groupId>
+    <artifactId>ttorrent</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>network</module>
+        <module>bencoding</module>
+        <module>ttorrent-tracker</module>
+        <module>ttorrent-client</module>
+        <module>common</module>
+        <module>tests</module>
+        <module>test-api</module>
+        <module>cli</module>
+    </modules>
+
+    <organization>
+        <name>Turn, Inc.</name>
+        <url>http://www.turn.com</url>
+    </organization>
+
+    <scm>
+        <connection>scm:git:git://github.com/turn/ttorrent.git</connection>
+        <url>http://github.com/turn/ttorrent</url>
+    </scm>
+
+    <licenses>
+        <license>
+            <name>Apache Software License version 2.0</name>
+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+        </license>
+    </licenses>
+
+    <issueManagement>
+        <system>GitHub</system>
+        <url>https://github.com/turn/ttorrent/issues</url>
+    </issueManagement>
+
+    <developers>
+        <developer>
+            <id>mpetazzoni</id>
+            <name>Maxime Petazzoni</name>
+            <email>mpetazzoni@turn.com</email>
+            <url>http://www.bulix.org</url>
+            <organization>Turn, Inc</organization>
+            <organizationUrl>http://www.turn.com</organizationUrl>
+            <roles>
+                <role>maintainer</role>
+                <role>architect</role>
+                <role>developer</role>
+            </roles>
+            <timezone>-8</timezone>
+            <properties>
+                <picUrl>https://secure.gravatar.com/avatar/6f705e0c299bca294444de3a6a3308b3</picUrl>
+            </properties>
+        </developer>
+    </developers>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <java.version>1.8</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+    </properties>
+
+    <repositories>
+        <repository>
+            <id>jboss-thirdparty-releases</id>
+            <name>JBoss Thirdparty Releases</name>
+            <url>https://repository.jboss.org/nexus/content/repositories/thirdparty-releases/</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.12.0</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.7</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.11</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.simpleframework</groupId>
+            <artifactId>simple</artifactId>
+            <version>4.1.21</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>1.6.4</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>6.8.8</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>net.sf</groupId>
+            <artifactId>jargs</artifactId>
+            <version>1.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations-java5</artifactId>
+            <version>RELEASE</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <defaultGoal>package</defaultGoal>
+        <directory>${basedir}/build</directory>
+
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>3.8.1</version>
+                    <configuration>
+                        <source>${java.version}</source>
+                        <target>${java.version}</target>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+<!--            <plugin>-->
+<!--                <groupId>org.apache.maven.plugins</groupId>-->
+<!--                <artifactId>maven-compiler-plugin</artifactId>-->
+<!--                <version>2.3.2</version>-->
+<!--                <configuration>-->
+<!--                    <source>1.6</source>-->
+<!--                    <target>1.6</target>-->
+<!--                </configuration>-->
+<!--            </plugin>-->
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.12.3</version>
+                <configuration>
+                    <argLine>-Xmx768M</argLine>
+                    <systemPropertyVariables>
+                        <com.turn.ttorrent.logLevel>${testLogLevel}</com.turn.ttorrent.logLevel>
+                        <java.nio.channels.spi.SelectorProvider>${java.nio.channels.spi.SelectorProvider}
+                        </java.nio.channels.spi.SelectorProvider>
+                        <buildDirectory>${project.build.directory}</buildDirectory>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-dependencies</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>false</overWriteSnapshots>
+                            <overWriteIfNewer>true</overWriteIfNewer>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <addClasspath>true</addClasspath>
+                            <!-- <classpathPrefix>lib</classpathPrefix> -->
+                            <!-- <mainClass>test.org.Cliente</mainClass> -->
+                        </manifest>
+                        <manifestEntries>
+                            <Class-Path>lib/</Class-Path>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+
+        </plugins>
+    </build>
+</project>
diff --git a/ttorrent-master/test-api/pom.xml b/ttorrent-master/test-api/pom.xml
new file mode 100644
index 0000000..b407722
--- /dev/null
+++ b/ttorrent-master/test-api/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/test-api</name>
+    <url>http://turn.github.com/ttorrent/</url>
+    <artifactId>ttorrent-test-api</artifactId>
+    <version>1.0</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-common</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/MockTimeService.java b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/MockTimeService.java
new file mode 100644
index 0000000..80806d7
--- /dev/null
+++ b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/MockTimeService.java
@@ -0,0 +1,17 @@
+package com.turn.ttorrent;
+
+import com.turn.ttorrent.common.TimeService;
+
+public class MockTimeService implements TimeService {
+
+  private volatile long time = 0;
+
+  @Override
+  public long now() {
+    return time;
+  }
+
+  public void setTime(long time) {
+    this.time = time;
+  }
+}
diff --git a/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/TempFiles.java b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/TempFiles.java
new file mode 100644
index 0000000..0ebed92
--- /dev/null
+++ b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/TempFiles.java
@@ -0,0 +1,142 @@
+package com.turn.ttorrent;
+
+import org.apache.commons.io.FileUtils;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * @author Pavel.Sher
+ * Date: 05.03.2008
+ */
+public class TempFiles {
+  private static final File ourCurrentTempDir = FileUtils.getTempDirectory();
+  private final File myCurrentTempDir;
+
+  private static Random ourRandom;
+
+  static {
+    ourRandom = new Random();
+    ourRandom.setSeed(System.currentTimeMillis());
+  }
+
+  private final List<File> myFilesToDelete = new ArrayList<File>();
+  private final Thread myShutdownHook;
+  private volatile boolean myInsideShutdownHook;
+
+  public TempFiles() {
+    myCurrentTempDir = ourCurrentTempDir;
+    if (!myCurrentTempDir.isDirectory()) {
+
+      throw new IllegalStateException("Temp directory is not a directory, was deleted by some process: " + myCurrentTempDir.getAbsolutePath() +
+              "\njava.io.tmpdir: " + FileUtils.getTempDirectory());
+    }
+
+    myShutdownHook = new Thread(new Runnable() {
+      public void run() {
+        myInsideShutdownHook = true;
+        cleanup();
+      }
+    });
+    Runtime.getRuntime().addShutdownHook(myShutdownHook);
+  }
+
+  private File doCreateTempDir(String prefix, String suffix) throws IOException {
+    prefix = prefix == null ? "" : prefix;
+    suffix = suffix == null ? ".tmp" : suffix;
+
+    do {
+      int count = ourRandom.nextInt();
+      final File f = new File(myCurrentTempDir, prefix + count + suffix);
+      if (!f.exists() && f.mkdirs()) {
+        return f.getCanonicalFile();
+      }
+    } while (true);
+
+  }
+
+  private File doCreateTempFile(String prefix, String suffix) throws IOException {
+    final File file = doCreateTempDir(prefix, suffix);
+    file.delete();
+    file.createNewFile();
+    return file;
+  }
+
+  public final File createTempFile() throws IOException {
+    File tempFile = doCreateTempFile("test", null);
+    registerAsTempFile(tempFile);
+    return tempFile;
+  }
+
+  public void registerAsTempFile(final File tempFile) {
+    myFilesToDelete.add(tempFile);
+  }
+
+  public final File createTempFile(int size) throws IOException {
+    File tempFile = createTempFile();
+    int bufLen = Math.min(8 * 1024, size);
+    final Random random = new Random();
+    if (bufLen == 0) return tempFile;
+    final OutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile));
+    try {
+      byte[] buf = new byte[bufLen];
+
+      int numWritten = 0;
+      for (int i = 0; i < size / buf.length; i++) {
+        random.nextBytes(buf);
+        fos.write(buf);
+        numWritten += buf.length;
+      }
+
+      if (size > numWritten) {
+        random.nextBytes(buf);
+        fos.write(buf, 0, size - numWritten);
+      }
+    } finally {
+      fos.close();
+    }
+
+    return tempFile;
+  }
+
+  /**
+   * Returns a File object for created temp directory.
+   * Also stores the value into this object accessed with {@link #getCurrentTempDir()}
+   *
+   * @return a File object for created temp directory
+   * @throws IOException if directory creation fails.
+   */
+  public final File createTempDir() throws IOException {
+    File f = doCreateTempDir("test", "");
+    registerAsTempFile(f);
+    return f;
+  }
+
+  /**
+   * Returns the current directory used by the test or null if no test is running or no directory is created yet.
+   *
+   * @return see above
+   */
+  public File getCurrentTempDir() {
+    return myCurrentTempDir;
+  }
+
+  public void cleanup() {
+    try {
+      for (File file : myFilesToDelete) {
+        try {
+          FileUtils.forceDelete(file);
+        } catch (IOException e) {
+        }
+      }
+
+      myFilesToDelete.clear();
+    } finally {
+      if (!myInsideShutdownHook) {
+        Runtime.getRuntime().removeShutdownHook(myShutdownHook);
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/Utils.java b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/Utils.java
new file mode 100644
index 0000000..a2ce0f1
--- /dev/null
+++ b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/Utils.java
@@ -0,0 +1,13 @@
+package com.turn.ttorrent;
+
+import org.apache.log4j.Level;
+
+public class Utils {
+
+  private final static String LOG_PROPERTY_KEY = "com.turn.ttorrent.logLevel";
+
+  public static Level getLogLevel() {
+    final String levelStr = System.getProperty(LOG_PROPERTY_KEY);
+    return Level.toLevel(levelStr, Level.INFO);
+  }
+}
diff --git a/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/WaitFor.java b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/WaitFor.java
new file mode 100644
index 0000000..d8b939b
--- /dev/null
+++ b/ttorrent-master/test-api/src/main/java/com/turn/ttorrent/WaitFor.java
@@ -0,0 +1,30 @@
+package com.turn.ttorrent;
+
+public abstract class WaitFor {
+  public static final long POLL_INTERVAL = 500;
+
+  private boolean myResult = false;
+
+  protected WaitFor() {
+    this(60 * 1000);
+  }
+
+  protected WaitFor(long timeout) {
+    long maxTime = System.currentTimeMillis() + timeout;
+    try {
+      while (System.currentTimeMillis() < maxTime && !condition()) {
+        Thread.sleep(POLL_INTERVAL);
+      }
+      if (condition()) {
+        myResult = true;
+      }
+    } catch (InterruptedException e) {
+    }
+  }
+
+  public boolean isMyResult() {
+    return myResult;
+  }
+
+  protected abstract boolean condition();
+}
diff --git a/ttorrent-master/tests/pom.xml b/ttorrent-master/tests/pom.xml
new file mode 100644
index 0000000..d9fecac
--- /dev/null
+++ b/ttorrent-master/tests/pom.xml
@@ -0,0 +1,40 @@
+<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-tests</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-tracker</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-client</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-test-api</artifactId>
+            <version>1.0</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/CommunicationManagerFactory.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/CommunicationManagerFactory.java
new file mode 100644
index 0000000..ed4be29
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/CommunicationManagerFactory.java
@@ -0,0 +1,51 @@
+package com.turn.ttorrent;
+
+import com.turn.ttorrent.client.CommunicationManager;
+import com.turn.ttorrent.common.LoggerUtils;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class CommunicationManagerFactory {
+
+  public final static int DEFAULT_POOL_SIZE = 10;
+
+  public CommunicationManager getClient(String name) {
+    final ExecutorService executorService = new ThreadPoolExecutor(
+            DEFAULT_POOL_SIZE, DEFAULT_POOL_SIZE,
+            0L, TimeUnit.MILLISECONDS,
+            new LinkedBlockingQueue<Runnable>(4000));
+    final ExecutorService pieceValidatorExecutor = new ThreadPoolExecutor(
+            DEFAULT_POOL_SIZE, DEFAULT_POOL_SIZE,
+            0L, TimeUnit.MILLISECONDS,
+            new LinkedBlockingQueue<Runnable>(400));
+    return new CommunicationManager(executorService, pieceValidatorExecutor) {
+      @Override
+      public void stop() {
+        super.stop();
+
+        int timeout = 60;
+        TimeUnit timeUnit = TimeUnit.SECONDS;
+
+        executorService.shutdown();
+        pieceValidatorExecutor.shutdown();
+        if (timeout > 0) {
+          try {
+            if (!pieceValidatorExecutor.awaitTermination(timeout, timeUnit)) {
+              logger.warn("unable to terminate executor service in {} {}", timeout, timeUnit);
+            }
+            boolean shutdownCorrectly = executorService.awaitTermination(timeout, timeUnit);
+            if (!shutdownCorrectly) {
+              logger.warn("unable to terminate executor service in {} {}", timeout, timeUnit);
+            }
+          } catch (InterruptedException e) {
+            LoggerUtils.warnAndDebugDetails(logger, "unable to await termination executor service, thread was interrupted", e);
+          }
+        }
+
+      }
+    };
+  }
+}
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/DummyPeerActivityListener.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/DummyPeerActivityListener.java
new file mode 100644
index 0000000..d8a08b7
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/DummyPeerActivityListener.java
@@ -0,0 +1,62 @@
+package com.turn.ttorrent;
+
+import com.turn.ttorrent.client.Piece;
+import com.turn.ttorrent.client.peer.PeerActivityListener;
+import com.turn.ttorrent.client.peer.SharingPeer;
+
+import java.io.IOException;
+import java.util.BitSet;
+
+public class DummyPeerActivityListener implements PeerActivityListener {
+
+
+  @Override
+  public void handlePeerChoked(SharingPeer peer) {
+
+  }
+
+  @Override
+  public void handlePeerReady(SharingPeer peer) {
+
+  }
+
+  @Override
+  public void afterPeerRemoved(SharingPeer peer) {
+
+  }
+
+  @Override
+  public void handlePieceAvailability(SharingPeer peer, Piece piece) {
+
+  }
+
+  @Override
+  public void handleBitfieldAvailability(SharingPeer peer, BitSet availablePieces) {
+
+  }
+
+  @Override
+  public void handlePieceSent(SharingPeer peer, Piece piece) {
+
+  }
+
+  @Override
+  public void handlePieceCompleted(SharingPeer peer, Piece piece) throws IOException {
+
+  }
+
+  @Override
+  public void handlePeerDisconnected(SharingPeer peer) {
+
+  }
+
+  @Override
+  public void handleIOException(SharingPeer peer, IOException ioe) {
+
+  }
+
+  @Override
+  public void handleNewPeerConnected(SharingPeer peer) {
+
+  }
+}
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/client/CommunicationManagerTest.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/client/CommunicationManagerTest.java
new file mode 100644
index 0000000..7cbc25b
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/client/CommunicationManagerTest.java
@@ -0,0 +1,1508 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.*;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.client.storage.EmptyPieceStorageFactory;
+import com.turn.ttorrent.client.storage.FileStorage;
+import com.turn.ttorrent.client.storage.FullyPieceStorageFactory;
+import com.turn.ttorrent.client.storage.PieceStorage;
+import com.turn.ttorrent.common.*;
+import com.turn.ttorrent.common.protocol.PeerMessage;
+import com.turn.ttorrent.network.FirstAvailableChannel;
+import com.turn.ttorrent.network.ServerChannelRegister;
+import com.turn.ttorrent.tracker.TrackedPeer;
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.turn.ttorrent.tracker.Tracker;
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.*;
+import java.net.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.ClosedByInterruptException;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.CRC32;
+import java.util.zip.Checksum;
+
+import static com.turn.ttorrent.CommunicationManagerFactory.DEFAULT_POOL_SIZE;
+import static com.turn.ttorrent.tracker.Tracker.ANNOUNCE_URL;
+import static org.testng.Assert.*;
+
+/**
+ * @author Sergey.Pak
+ * Date: 7/26/13
+ * Time: 2:32 PM
+ */
+@Test(timeOut = 600000)
+public class CommunicationManagerTest {
+
+  private CommunicationManagerFactory communicationManagerFactory;
+
+  private List<CommunicationManager> communicationManagerList;
+  private static final String TEST_RESOURCES = "src/test/resources";
+  private Tracker tracker;
+  private TempFiles tempFiles;
+
+  public CommunicationManagerTest() {
+    communicationManagerFactory = new CommunicationManagerFactory();
+    if (Logger.getRootLogger().getAllAppenders().hasMoreElements())
+      return;
+    BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("[%d{MMdd HH:mm:ss,SSS} %t] %6p - %20.20c - %m %n")));
+  }
+
+  @BeforeMethod
+  public void setUp() throws IOException {
+    tempFiles = new TempFiles();
+    communicationManagerList = new ArrayList<CommunicationManager>();
+    Logger.getRootLogger().setLevel(Utils.getLogLevel());
+    startTracker();
+  }
+
+  private void saveTorrent(TorrentMetadata torrent, File file) throws IOException {
+    FileOutputStream fos = new FileOutputStream(file);
+    fos.write(new TorrentSerializer().serialize(torrent));
+    fos.close();
+  }
+
+  public void testThatSeederIsNotReceivedHaveMessages() throws Exception {
+    final ExecutorService workerES = Executors.newFixedThreadPool(10);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    final AtomicBoolean isSeederReceivedHaveMessage = new AtomicBoolean(false);
+    CommunicationManager seeder = new CommunicationManager(workerES, validatorES) {
+
+      @Override
+      public SharingPeer createSharingPeer(String host, int port, ByteBuffer peerId, SharedTorrent torrent, ByteChannel channel, String clientIdentifier, int clientVersion) {
+        return new SharingPeer(host, port, peerId, torrent, getConnectionManager(), this, channel, "TO", 1234) {
+          @Override
+          public synchronized void handleMessage(PeerMessage msg) {
+            if (msg instanceof PeerMessage.HaveMessage) {
+              isSeederReceivedHaveMessage.set(true);
+            }
+            super.handleMessage(msg);
+          }
+        };
+      }
+
+      @Override
+      public void stop() {
+        super.stop();
+        workerES.shutdown();
+        validatorES.shutdown();
+      }
+    };
+    communicationManagerList.add(seeder);
+
+    File tempFile = tempFiles.createTempFile(100 * 1025 * 1024);
+    URL announce = new URL("http://127.0.0.1:6969/announce");
+    URI announceURI = announce.toURI();
+
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, announceURI, "Test");
+    File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+
+    seeder.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParent());
+    seeder.start(InetAddress.getLocalHost());
+
+    waitForSeederIsAnnounsedOnTracker(torrent.getHexInfoHash());
+
+    CommunicationManager leecher = createClient();
+    leecher.start(InetAddress.getLocalHost());
+    TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath());
+    waitDownloadComplete(torrentManager, 15);
+    assertFalse(isSeederReceivedHaveMessage.get());
+  }
+
+  private void waitDownloadComplete(TorrentManager torrentManager, int timeoutSec) throws InterruptedException {
+    final Semaphore semaphore = new Semaphore(0);
+    TorrentListenerWrapper listener = new TorrentListenerWrapper() {
+      @Override
+      public void downloadComplete() {
+        semaphore.release();
+      }
+    };
+    try {
+      torrentManager.addListener(listener);
+      boolean res = semaphore.tryAcquire(timeoutSec, TimeUnit.SECONDS);
+      if (!res) throw new RuntimeException("Unable to download file in " + timeoutSec + " seconds");
+    } finally {
+      torrentManager.removeListener(listener);
+    }
+  }
+
+  private void waitForSeederIsAnnounsedOnTracker(final String hexInfoHash) {
+    final WaitFor waitFor = new WaitFor(10 * 1000) {
+      @Override
+      protected boolean condition() {
+        return tracker.getTrackedTorrent(hexInfoHash) != null;
+      }
+    };
+    assertTrue(waitFor.isMyResult());
+  }
+
+
+  //  @Test(invocationCount = 50)
+  public void download_multiple_files() throws IOException, InterruptedException, URISyntaxException {
+    int numFiles = 50;
+    this.tracker.setAcceptForeignTorrents(true);
+
+    final File srcDir = tempFiles.createTempDir();
+    final File downloadDir = tempFiles.createTempDir();
+
+    CommunicationManager seeder = createClient("seeder");
+    seeder.start(InetAddress.getLocalHost());
+    CommunicationManager leech = null;
+
+
+    try {
+      URL announce = new URL("http://127.0.0.1:6969/announce");
+      URI announceURI = announce.toURI();
+      final Set<String> names = new HashSet<String>();
+      List<File> filesToShare = new ArrayList<File>();
+      for (int i = 0; i < numFiles; i++) {
+        File tempFile = tempFiles.createTempFile(513 * 1024);
+        File srcFile = new File(srcDir, tempFile.getName());
+        assertTrue(tempFile.renameTo(srcFile));
+
+        TorrentMetadata torrent = TorrentCreator.create(srcFile, announceURI, "Test");
+        File torrentFile = new File(srcFile.getParentFile(), srcFile.getName() + ".torrent");
+        saveTorrent(torrent, torrentFile);
+        filesToShare.add(srcFile);
+        names.add(srcFile.getName());
+      }
+
+      for (File f : filesToShare) {
+        File torrentFile = new File(f.getParentFile(), f.getName() + ".torrent");
+        seeder.addTorrent(torrentFile.getAbsolutePath(), f.getParent());
+      }
+      leech = createClient("leecher");
+      leech.start(new InetAddress[]{InetAddress.getLocalHost()}, 5, null, new SelectorFactoryImpl());
+      for (File f : filesToShare) {
+        File torrentFile = new File(f.getParentFile(), f.getName() + ".torrent");
+        leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath());
+      }
+
+      new WaitFor(60 * 1000) {
+        @Override
+        protected boolean condition() {
+
+          final Set<String> strings = listFileNames(downloadDir);
+          int count = 0;
+          final List<String> partItems = new ArrayList<String>();
+          for (String s : strings) {
+            if (s.endsWith(".part")) {
+              count++;
+              partItems.add(s);
+            }
+          }
+          if (count < 5) {
+
+            System.err.printf("Count: %d. Items: %s%n", count, Arrays.toString(partItems.toArray()));
+          }
+          return strings.containsAll(names);
+        }
+      };
+
+      assertEquals(listFileNames(downloadDir), names);
+    } finally {
+      leech.stop();
+      seeder.stop();
+    }
+  }
+
+  private Set<String> listFileNames(File downloadDir) {
+    if (downloadDir == null) return Collections.emptySet();
+    Set<String> names = new HashSet<String>();
+    File[] files = downloadDir.listFiles();
+    if (files == null) return Collections.emptySet();
+    for (File f : files) {
+      names.add(f.getName());
+    }
+    return names;
+  }
+
+  public void testHungSeeder() throws Exception {
+    this.tracker.setAcceptForeignTorrents(true);
+
+    File tempFile = tempFiles.createTempFile(500 * 1025 * 1024);
+    URL announce = new URL("http://127.0.0.1:6969/announce");
+    URI announceURI = announce.toURI();
+
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, announceURI, "Test");
+    File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+
+    CommunicationManager goodSeeder = createClient();
+    goodSeeder.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParent(), FullyPieceStorageFactory.INSTANCE);
+
+    final ExecutorService es = Executors.newFixedThreadPool(10);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    CommunicationManager hungSeeder = new CommunicationManager(es, validatorES) {
+      @Override
+      public void stop() {
+        super.stop();
+        es.shutdownNow();
+        validatorES.shutdownNow();
+      }
+
+      @Override
+      public SharingPeer createSharingPeer(String host, int port, ByteBuffer peerId, SharedTorrent torrent, ByteChannel channel, String clientIdentifier, int clientVersion) {
+        return new SharingPeer(host, port, peerId, torrent, getConnectionManager(), this, channel, clientIdentifier, clientVersion) {
+          @Override
+          public void handleMessage(PeerMessage msg) {
+            if (msg instanceof PeerMessage.RequestMessage) {
+              return;
+            }
+            super.handleMessage(msg);
+          }
+        };
+      }
+    };
+    hungSeeder.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParent(), FullyPieceStorageFactory.INSTANCE);
+
+    final File downloadDir = tempFiles.createTempDir();
+    CommunicationManager leech = createClient();
+    leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath(), EmptyPieceStorageFactory.INSTANCE);
+
+    try {
+      hungSeeder.start(InetAddress.getLocalHost());
+      goodSeeder.start(InetAddress.getLocalHost());
+      leech.start(InetAddress.getLocalHost());
+
+      waitForFileInDir(downloadDir, tempFile.getName());
+      assertFilesEqual(tempFile, new File(downloadDir, tempFile.getName()));
+    } finally {
+      goodSeeder.stop();
+      leech.stop();
+    }
+  }
+
+  public void large_file_download() throws IOException, URISyntaxException, InterruptedException {
+    this.tracker.setAcceptForeignTorrents(true);
+
+    File tempFile = tempFiles.createTempFile(201 * 1025 * 1024);
+    URL announce = new URL("http://127.0.0.1:6969/announce");
+    URI announceURI = announce.toURI();
+
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, announceURI, "Test");
+    File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+
+    CommunicationManager seeder = createClient();
+    seeder.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParent(), FullyPieceStorageFactory.INSTANCE);
+
+    final File downloadDir = tempFiles.createTempDir();
+    CommunicationManager leech = createClient();
+    leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath(), EmptyPieceStorageFactory.INSTANCE);
+
+    try {
+      seeder.start(InetAddress.getLocalHost());
+      leech.start(InetAddress.getLocalHost());
+
+      waitForFileInDir(downloadDir, tempFile.getName());
+      assertFilesEqual(tempFile, new File(downloadDir, tempFile.getName()));
+    } finally {
+      seeder.stop();
+      leech.stop();
+    }
+  }
+
+  // TODO: 24.09.2018 flaky test, it's needed to debug and fix
+  @Test(enabled = false)
+  public void testManyLeechers() throws IOException, URISyntaxException, InterruptedException {
+    this.tracker.setAcceptForeignTorrents(true);
+
+    File tempFile = tempFiles.createTempFile(400 * 1025 * 1024);
+    URL announce = new URL("http://127.0.0.1:6969/announce");
+    URI announceURI = announce.toURI();
+
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, announceURI, "Test");
+    File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+
+    CommunicationManager seeder = createClient();
+    seeder.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParent(), FullyPieceStorageFactory.INSTANCE);
+
+    List<Map.Entry<CommunicationManager, File>> leechers = new ArrayList<Map.Entry<CommunicationManager, File>>();
+    for (int i = 0; i < 4; i++) {
+      final File downloadDir = tempFiles.createTempDir();
+      CommunicationManager leech = createClient();
+      leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath(), EmptyPieceStorageFactory.INSTANCE);
+      leechers.add(new AbstractMap.SimpleEntry<CommunicationManager, File>(leech, downloadDir));
+    }
+
+    try {
+      seeder.start(InetAddress.getLocalHost());
+      for (Map.Entry<CommunicationManager, File> entry : leechers) {
+        entry.getKey().start(InetAddress.getLocalHost());
+      }
+
+      for (Map.Entry<CommunicationManager, File> leecher : leechers) {
+        File downloadDir = leecher.getValue();
+        waitForFileInDir(downloadDir, tempFile.getName());
+        assertFilesEqual(tempFile, new File(downloadDir, tempFile.getName()));
+      }
+    } finally {
+      seeder.stop();
+      for (Map.Entry<CommunicationManager, File> e : leechers) {
+        e.getKey().stop();
+      }
+    }
+  }
+
+  @Test(enabled = false)
+  public void endgameModeTest() throws Exception {
+    this.tracker.setAcceptForeignTorrents(true);
+    final int numSeeders = 2;
+    List<CommunicationManager> seeders = new ArrayList<CommunicationManager>();
+    final AtomicInteger skipPiecesCount = new AtomicInteger(1);
+    for (int i = 0; i < numSeeders; i++) {
+      final ExecutorService es = Executors.newFixedThreadPool(10);
+      final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+      final CommunicationManager seeder = new CommunicationManager(es, validatorES) {
+        @Override
+        public void stop() {
+          super.stop();
+          es.shutdownNow();
+          validatorES.shutdownNow();
+        }
+
+        @Override
+        public SharingPeer createSharingPeer(String host, int port, ByteBuffer peerId, SharedTorrent torrent, ByteChannel channel, String clientIdentifier, int clientVersion) {
+          return new SharingPeer(host, port, peerId, torrent, getConnectionManager(), this, channel, "TO", 1234) {
+            @Override
+            public void send(PeerMessage message) throws IllegalStateException {
+              if (message instanceof PeerMessage.PieceMessage) {
+                if (skipPiecesCount.getAndDecrement() > 0) {
+                  return;
+                }
+              }
+              super.send(message);
+            }
+          };
+        }
+      };
+      seeders.add(seeder);
+      communicationManagerList.add(seeder);
+    }
+    File tempFile = tempFiles.createTempFile(1024 * 20 * 1024);
+
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, this.tracker.getAnnounceURI(), "Test");
+    File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+
+    for (int i = 0; i < numSeeders; i++) {
+      CommunicationManager communicationManager = seeders.get(i);
+      communicationManager.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParent());
+      communicationManager.start(InetAddress.getLocalHost());
+    }
+
+    final File downloadDir = tempFiles.createTempDir();
+    CommunicationManager leech = createClient();
+    leech.start(InetAddress.getLocalHost());
+    TorrentManager torrentManager = leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getParent());
+    waitDownloadComplete(torrentManager, 20);
+
+    waitForFileInDir(downloadDir, tempFile.getName());
+
+  }
+
+
+  public void more_than_one_seeder_for_same_torrent() throws IOException, InterruptedException {
+    this.tracker.setAcceptForeignTorrents(true);
+    assertEquals(0, this.tracker.getTrackedTorrents().size());
+
+    final int numSeeders = 5;
+    List<CommunicationManager> seeders = new ArrayList<CommunicationManager>();
+    for (int i = 0; i < numSeeders; i++) {
+      seeders.add(createClient());
+    }
+
+    try {
+      File tempFile = tempFiles.createTempFile(100 * 1024);
+
+      TorrentMetadata torrent = TorrentCreator.create(tempFile, this.tracker.getAnnounceURI(), "Test");
+      File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+      saveTorrent(torrent, torrentFile);
+
+      for (int i = 0; i < numSeeders; i++) {
+        CommunicationManager communicationManager = seeders.get(i);
+        communicationManager.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParent());
+        communicationManager.start(InetAddress.getLocalHost());
+      }
+
+      new WaitFor() {
+        @Override
+        protected boolean condition() {
+          for (TrackedTorrent tt : tracker.getTrackedTorrents()) {
+            if (tt.getPeers().size() == numSeeders) return true;
+          }
+
+          return false;
+        }
+      };
+
+      Collection<TrackedTorrent> torrents = this.tracker.getTrackedTorrents();
+      assertEquals(torrents.size(), 1);
+      assertEquals(numSeeders, torrents.iterator().next().seeders());
+    } finally {
+      for (CommunicationManager communicationManager : seeders) {
+        communicationManager.stop();
+      }
+    }
+
+  }
+
+  public void testThatDownloadStatisticProvidedToTracker() throws Exception {
+    final ExecutorService executorService = Executors.newFixedThreadPool(8);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    final AtomicInteger countOfTrackerResponses = new AtomicInteger(0);
+    CommunicationManager leecher = new CommunicationManager(executorService, validatorES) {
+      @Override
+      public void handleDiscoveredPeers(List<Peer> peers, String hexInfoHash) {
+        super.handleDiscoveredPeers(peers, hexInfoHash);
+        countOfTrackerResponses.incrementAndGet();
+      }
+
+      @Override
+      public void stop() {
+        super.stop();
+        executorService.shutdownNow();
+        validatorES.shutdownNow();
+      }
+    };
+
+    communicationManagerList.add(leecher);
+
+    final int fileSize = 2 * 1025 * 1024;
+    File tempFile = tempFiles.createTempFile(fileSize);
+    URL announce = new URL("http://127.0.0.1:6969/announce");
+    URI announceURI = announce.toURI();
+
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, announceURI, "Test");
+    File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+
+    String hash = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath()).getHexInfoHash();
+    leecher.start(InetAddress.getLocalHost());
+    final LoadedTorrent announceableTorrent = leecher.getTorrentsStorage().getLoadedTorrent(hash);
+
+    final SharedTorrent sharedTorrent = leecher.getTorrentsStorage().putIfAbsentActiveTorrent(announceableTorrent.getTorrentHash().getHexInfoHash(),
+            leecher.getTorrentLoader().loadTorrent(announceableTorrent));
+
+    sharedTorrent.init();
+
+    new WaitFor(10 * 1000) {
+      @Override
+      protected boolean condition() {
+        return countOfTrackerResponses.get() == 1;
+      }
+    };
+
+    final TrackedTorrent trackedTorrent = tracker.getTrackedTorrent(announceableTorrent.getTorrentHash().getHexInfoHash());
+
+    assertEquals(trackedTorrent.getPeers().size(), 1);
+
+    final TrackedPeer trackedPeer = trackedTorrent.getPeers().values().iterator().next();
+
+    assertEquals(trackedPeer.getUploaded(), 0);
+    assertEquals(trackedPeer.getDownloaded(), 0);
+    assertEquals(trackedPeer.getLeft(), fileSize);
+
+    Piece piece = sharedTorrent.getPiece(1);
+    sharedTorrent.handlePieceCompleted(null, piece);
+    sharedTorrent.markCompleted(piece);
+
+    new WaitFor(10 * 1000) {
+      @Override
+      protected boolean condition() {
+        return countOfTrackerResponses.get() >= 2;
+      }
+    };
+    int downloaded = 512 * 1024;//one piece
+    assertEquals(trackedPeer.getUploaded(), 0);
+    assertEquals(trackedPeer.getDownloaded(), downloaded);
+    assertEquals(trackedPeer.getLeft(), fileSize - downloaded);
+  }
+
+  public void no_full_seeder_test() throws IOException, URISyntaxException, InterruptedException, NoSuchAlgorithmException {
+    this.tracker.setAcceptForeignTorrents(true);
+
+    final int pieceSize = 48 * 1024; // lower piece size to reduce disk usage
+    final int numSeeders = 6;
+    final int piecesCount = numSeeders * 3 + 15;
+
+    final List<CommunicationManager> clientsList;
+    clientsList = new ArrayList<CommunicationManager>(piecesCount);
+
+    final MessageDigest md5 = MessageDigest.getInstance("MD5");
+
+    try {
+      File tempFile = tempFiles.createTempFile(piecesCount * pieceSize);
+      List<File> targetFiles = new ArrayList<File>();
+
+      String hash = createMultipleSeedersWithDifferentPieces(tempFile, piecesCount, pieceSize, numSeeders, clientsList, targetFiles);
+      String baseMD5 = getFileMD5(tempFile, md5);
+      assertEquals(numSeeders, targetFiles.size());
+
+      validateMultipleClientsResults(clientsList, md5, tempFile, baseMD5, hash, targetFiles);
+
+    } finally {
+      for (CommunicationManager communicationManager : clientsList) {
+        communicationManager.stop();
+      }
+    }
+  }
+
+  @Test(enabled = false)
+  public void corrupted_seeder_repair() throws IOException, URISyntaxException, InterruptedException, NoSuchAlgorithmException {
+    this.tracker.setAcceptForeignTorrents(true);
+
+    final int pieceSize = 48 * 1024; // lower piece size to reduce disk usage
+    final int numSeeders = 6;
+    final int piecesCount = numSeeders + 7;
+
+    final List<CommunicationManager> clientsList;
+    clientsList = new ArrayList<CommunicationManager>(piecesCount);
+
+    final MessageDigest md5 = MessageDigest.getInstance("MD5");
+
+    try {
+      File baseFile = tempFiles.createTempFile(piecesCount * pieceSize);
+      List<File> targetFiles = new ArrayList<File>();
+
+      String hash = createMultipleSeedersWithDifferentPieces(baseFile, piecesCount, pieceSize, numSeeders, clientsList, targetFiles);
+      assertEquals(numSeeders, targetFiles.size());
+      String baseMD5 = getFileMD5(baseFile, md5);
+      final CommunicationManager firstCommunicationManager = clientsList.get(0);
+
+      new WaitFor(10 * 1000) {
+        @Override
+        protected boolean condition() {
+          return firstCommunicationManager.getTorrentsStorage().activeTorrents().size() >= 1;
+        }
+      };
+
+      final SharedTorrent torrent = firstCommunicationManager.getTorrents().iterator().next();
+      final File file = new File(targetFiles.get(0).getParentFile(), TorrentUtils.getTorrentFileNames(torrent).get(0));
+      final int oldByte;
+      {
+        RandomAccessFile raf = new RandomAccessFile(file, "rw");
+        raf.seek(0);
+        oldByte = raf.read();
+        raf.seek(0);
+        // replacing the byte
+        if (oldByte != 35) {
+          raf.write(35);
+        } else {
+          raf.write(45);
+        }
+        raf.close();
+      }
+      final WaitFor waitFor = new WaitFor(60 * 1000) {
+        @Override
+        protected boolean condition() {
+          for (CommunicationManager client : clientsList) {
+            final SharedTorrent next = client.getTorrents().iterator().next();
+            if (next.getCompletedPieces().cardinality() < next.getPieceCount() - 1) {
+              return false;
+            }
+          }
+          return true;
+        }
+      };
+
+      if (!waitFor.isMyResult()) {
+        fail("All seeders didn't get their files");
+      }
+      Thread.sleep(10 * 1000);
+      {
+        byte[] piece = new byte[pieceSize];
+        FileInputStream fin = new FileInputStream(baseFile);
+        fin.read(piece);
+        fin.close();
+        RandomAccessFile raf;
+        try {
+          raf = new RandomAccessFile(file, "rw");
+          raf.seek(0);
+          raf.write(oldByte);
+          raf.close();
+        } catch (FileNotFoundException e) {
+          e.printStackTrace();
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+      }
+      validateMultipleClientsResults(clientsList, md5, baseFile, baseMD5, hash, targetFiles);
+
+    } finally {
+      for (CommunicationManager communicationManager : clientsList) {
+        communicationManager.stop();
+      }
+    }
+  }
+
+  public void testThatTorrentsHaveLazyInitAndRemovingAfterDownload()
+          throws IOException, InterruptedException, URISyntaxException {
+    final CommunicationManager seeder = createClient();
+    File tempFile = tempFiles.createTempFile(100 * 1025 * 1024);
+    URL announce = new URL("http://127.0.0.1:6969/announce");
+    URI announceURI = announce.toURI();
+
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, announceURI, "Test");
+    File torrentFile = new File(tempFile.getParentFile(), tempFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+    seeder.addTorrent(torrentFile.getAbsolutePath(), tempFile.getParentFile().getAbsolutePath());
+
+    final CommunicationManager leecher = createClient();
+    File downloadDir = tempFiles.createTempDir();
+    leecher.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath());
+    seeder.start(InetAddress.getLocalHost());
+
+    assertEquals(1, seeder.getTorrentsStorage().announceableTorrents().size());
+    assertEquals(0, seeder.getTorrentsStorage().activeTorrents().size());
+    assertEquals(0, leecher.getTorrentsStorage().activeTorrents().size());
+
+    leecher.start(InetAddress.getLocalHost());
+
+    WaitFor waitFor = new WaitFor(10 * 1000) {
+
+      @Override
+      protected boolean condition() {
+        return seeder.getTorrentsStorage().activeTorrents().size() == 1 &&
+                leecher.getTorrentsStorage().activeTorrents().size() == 1;
+      }
+    };
+
+    assertTrue(waitFor.isMyResult(), "Torrent was not successfully initialized");
+
+    assertEquals(1, seeder.getTorrentsStorage().activeTorrents().size());
+    assertEquals(1, leecher.getTorrentsStorage().activeTorrents().size());
+
+    waitForFileInDir(downloadDir, tempFile.getName());
+    assertFilesEqual(tempFile, new File(downloadDir, tempFile.getName()));
+
+    waitFor = new WaitFor(10 * 1000) {
+
+      @Override
+      protected boolean condition() {
+        return seeder.getTorrentsStorage().activeTorrents().size() == 0 &&
+                leecher.getTorrentsStorage().activeTorrents().size() == 0;
+      }
+    };
+
+    assertTrue(waitFor.isMyResult(), "Torrent was not successfully removed");
+
+    assertEquals(0, seeder.getTorrentsStorage().activeTorrents().size());
+    assertEquals(0, leecher.getTorrentsStorage().activeTorrents().size());
+
+  }
+
+  public void corrupted_seeder() throws IOException, InterruptedException, NoSuchAlgorithmException {
+    this.tracker.setAcceptForeignTorrents(true);
+
+    final int pieceSize = 48 * 1024; // lower piece size to reduce disk usage
+    final int piecesCount = 35;
+
+    final List<CommunicationManager> clientsList;
+    clientsList = new ArrayList<CommunicationManager>(piecesCount);
+
+    final MessageDigest md5 = MessageDigest.getInstance("MD5");
+
+    try {
+      final File baseFile = tempFiles.createTempFile(piecesCount * pieceSize);
+      final File badFile = tempFiles.createTempFile(piecesCount * pieceSize);
+
+      final CommunicationManager communicationManager2 = createAndStartClient();
+      final File client2Dir = tempFiles.createTempDir();
+      final File client2File = new File(client2Dir, baseFile.getName());
+      FileUtils.copyFile(badFile, client2File);
+
+      final TorrentMetadata torrent = TorrentCreator.create(baseFile, null, this.tracker.getAnnounceURI(), null, "Test", pieceSize);
+      final File torrentFile = tempFiles.createTempFile();
+      saveTorrent(torrent, torrentFile);
+
+      communicationManager2.addTorrent(torrentFile.getAbsolutePath(), client2Dir.getAbsolutePath());
+
+      final CommunicationManager leech = createAndStartClient();
+      final File leechDestDir = tempFiles.createTempDir();
+      final AtomicReference<Exception> thrownException = new AtomicReference<Exception>();
+      final Thread th = new Thread(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            TorrentManager torrentManager = leech.addTorrent(torrentFile.getAbsolutePath(), leechDestDir.getAbsolutePath());
+            waitDownloadComplete(torrentManager, 10);
+          } catch (Exception e) {
+            thrownException.set(e);
+            throw new RuntimeException(e);
+          }
+        }
+      });
+      th.start();
+      final WaitFor waitFor = new WaitFor(30 * 1000) {
+        @Override
+        protected boolean condition() {
+          return th.getState() == Thread.State.TERMINATED;
+        }
+      };
+
+      final Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
+      for (Map.Entry<Thread, StackTraceElement[]> entry : allStackTraces.entrySet()) {
+        System.out.printf("%s:%n", entry.getKey().getName());
+        for (StackTraceElement elem : entry.getValue()) {
+          System.out.println(elem.toString());
+        }
+      }
+
+      assertTrue(waitFor.isMyResult());
+      assertNotNull(thrownException.get());
+      assertTrue(thrownException.get().getMessage().contains("Unable to download"));
+
+    } finally {
+      for (CommunicationManager communicationManager : clientsList) {
+        communicationManager.stop();
+      }
+    }
+  }
+
+  public void unlock_file_when_no_leechers() throws InterruptedException, IOException {
+    CommunicationManager seeder = createClient();
+    tracker.setAcceptForeignTorrents(true);
+
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 7);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    seeder.start(InetAddress.getLocalHost());
+
+    downloadAndStop(torrent, 15 * 1000, createClient());
+    Thread.sleep(2 * 1000);
+    assertTrue(dwnlFile.exists() && dwnlFile.isFile());
+    final boolean delete = dwnlFile.delete();
+    assertTrue(delete && !dwnlFile.exists());
+  }
+
+  public void download_many_times() throws InterruptedException, IOException {
+    CommunicationManager seeder = createClient();
+    tracker.setAcceptForeignTorrents(true);
+
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 7);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    seeder.start(InetAddress.getLocalHost());
+
+    for (int i = 0; i < 5; i++) {
+      downloadAndStop(torrent, 250 * 1000, createClient());
+      Thread.sleep(3 * 1000);
+    }
+  }
+
+  public void testConnectToAllDiscoveredPeers() throws Exception {
+    tracker.setAcceptForeignTorrents(true);
+
+    final ExecutorService executorService = Executors.newFixedThreadPool(8);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    CommunicationManager leecher = new CommunicationManager(executorService, validatorES) {
+      @Override
+      public void stop() {
+        super.stop();
+        executorService.shutdownNow();
+        validatorES.shutdownNow();
+      }
+    };
+    leecher.setMaxInConnectionsCount(10);
+    leecher.setMaxOutConnectionsCount(10);
+
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 34);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+
+    final String hexInfoHash = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath()).getHexInfoHash();
+    final List<ServerSocket> serverSockets = new ArrayList<ServerSocket>();
+
+    final int startPort = 6885;
+    int port = startPort;
+    PeerUID[] peerUids = new PeerUID[]{
+            new PeerUID(new InetSocketAddress("127.0.0.1", port++), hexInfoHash),
+            new PeerUID(new InetSocketAddress("127.0.0.1", port++), hexInfoHash),
+            new PeerUID(new InetSocketAddress("127.0.0.1", port), hexInfoHash)
+    };
+    final ExecutorService es = Executors.newSingleThreadExecutor();
+    try {
+      leecher.start(InetAddress.getLocalHost());
+
+      WaitFor waitFor = new WaitFor(5000) {
+        @Override
+        protected boolean condition() {
+          return tracker.getTrackedTorrent(hexInfoHash) != null;
+        }
+      };
+
+      assertTrue(waitFor.isMyResult());
+
+      final TrackedTorrent trackedTorrent = tracker.getTrackedTorrent(hexInfoHash);
+      Map<PeerUID, TrackedPeer> trackedPeerMap = new HashMap<PeerUID, TrackedPeer>();
+
+      port = startPort;
+      for (PeerUID uid : peerUids) {
+        trackedPeerMap.put(uid, new TrackedPeer(trackedTorrent, "127.0.0.1", port, ByteBuffer.wrap("id".getBytes(Constants.BYTE_ENCODING))));
+        serverSockets.add(new ServerSocket(port));
+        port++;
+      }
+
+      trackedTorrent.getPeers().putAll(trackedPeerMap);
+
+      //wait until all server sockets accept connection from leecher
+      for (final ServerSocket ss : serverSockets) {
+        final Future<?> future = es.submit(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              final Socket socket = ss.accept();
+              socket.close();
+            } catch (IOException e) {
+              throw new RuntimeException("can not accept connection");
+            }
+          }
+        });
+        try {
+          future.get(10, TimeUnit.SECONDS);
+        } catch (ExecutionException e) {
+          fail("get execution exception on accept connection", e);
+        } catch (TimeoutException e) {
+          fail("not received connection from leecher in specified timeout", e);
+        }
+      }
+
+    } finally {
+      for (ServerSocket ss : serverSockets) {
+        try {
+          ss.close();
+        } catch (IOException e) {
+          fail("can not close server socket", e);
+        }
+      }
+      es.shutdown();
+      leecher.stop();
+    }
+  }
+
+  public void download_io_error() throws InterruptedException, IOException {
+    tracker.setAcceptForeignTorrents(true);
+    CommunicationManager seeder = createClient();
+
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 34);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    seeder.start(InetAddress.getLocalHost());
+
+    final AtomicInteger interrupts = new AtomicInteger(0);
+    final ExecutorService es = Executors.newFixedThreadPool(DEFAULT_POOL_SIZE);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    final CommunicationManager leech = new CommunicationManager(es, validatorES) {
+      @Override
+      public void handlePieceCompleted(SharingPeer peer, Piece piece) throws IOException {
+        super.handlePieceCompleted(peer, piece);
+        if (piece.getIndex() % 4 == 0 && interrupts.incrementAndGet() <= 2) {
+          peer.unbind(true);
+        }
+      }
+
+      @Override
+      public void stop(int timeout, TimeUnit timeUnit) {
+        super.stop(timeout, timeUnit);
+        es.shutdown();
+        validatorES.shutdown();
+      }
+    };
+    //manually add leech here for graceful shutdown.
+    communicationManagerList.add(leech);
+    downloadAndStop(torrent, 45 * 1000, leech);
+    Thread.sleep(2 * 1000);
+  }
+
+  public void download_uninterruptibly_positive() throws InterruptedException, IOException {
+    tracker.setAcceptForeignTorrents(true);
+    CommunicationManager seeder = createClient();
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 24);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seeder.start(InetAddress.getLocalHost());
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    CommunicationManager leecher = createClient();
+    leecher.start(InetAddress.getLocalHost());
+    TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath());
+    waitDownloadComplete(torrentManager, 10);
+  }
+
+  public void download_uninterruptibly_negative() throws InterruptedException, IOException {
+    tracker.setAcceptForeignTorrents(true);
+    final AtomicInteger downloadedPiecesCount = new AtomicInteger(0);
+    final CommunicationManager seeder = createClient();
+
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 24);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seeder.start(InetAddress.getLocalHost());
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    final ExecutorService es = Executors.newFixedThreadPool(DEFAULT_POOL_SIZE);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    final CommunicationManager leecher = new CommunicationManager(es, validatorES) {
+      @Override
+      public void stop(int timeout, TimeUnit timeUnit) {
+        super.stop(timeout, timeUnit);
+        es.shutdown();
+        validatorES.shutdown();
+      }
+
+      @Override
+      public void handlePieceCompleted(SharingPeer peer, Piece piece) throws IOException {
+        super.handlePieceCompleted(peer, piece);
+        if (downloadedPiecesCount.incrementAndGet() > 10) {
+          seeder.stop();
+        }
+      }
+    };
+    communicationManagerList.add(leecher);
+    leecher.start(InetAddress.getLocalHost());
+    final File destDir = tempFiles.createTempDir();
+    TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), destDir.getAbsolutePath());
+    try {
+      waitDownloadComplete(torrentManager, 7);
+      fail("Must fail, because file wasn't downloaded completely");
+    } catch (RuntimeException ex) {
+
+      LoadedTorrent loadedTorrent = leecher.getTorrentsStorage().getLoadedTorrent(torrentManager.getHexInfoHash());
+      loadedTorrent.getPieceStorage().close();
+
+      // delete .part file
+      File[] destDirFiles = destDir.listFiles();
+      assertNotNull(destDirFiles);
+      assertEquals(1, destDirFiles.length);
+      File targetFile = destDirFiles[0];
+      if (!targetFile.delete()) {
+        fail("Unable to remove file " + targetFile);
+      }
+      // ensure .part was deleted:
+      destDirFiles = destDir.listFiles();
+      assertNotNull(destDirFiles);
+      assertEquals(0, destDirFiles.length);
+    }
+
+  }
+
+  public void download_uninterruptibly_timeout() throws InterruptedException, IOException {
+    tracker.setAcceptForeignTorrents(true);
+    CommunicationManager seeder = createClient();
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 24);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seeder.start(InetAddress.getLocalHost());
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    final AtomicInteger piecesDownloaded = new AtomicInteger(0);
+    final ExecutorService es = Executors.newFixedThreadPool(DEFAULT_POOL_SIZE);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    CommunicationManager leecher = new CommunicationManager(es, validatorES) {
+      @Override
+      public void handlePieceCompleted(SharingPeer peer, Piece piece) {
+        piecesDownloaded.incrementAndGet();
+        try {
+          Thread.sleep(piecesDownloaded.get() * 500);
+        } catch (InterruptedException ignored) {
+
+        }
+      }
+
+      @Override
+      public void stop(int timeout, TimeUnit timeUnit) {
+        super.stop(timeout, timeUnit);
+        es.shutdown();
+        validatorES.shutdown();
+      }
+    };
+    communicationManagerList.add(leecher);
+    leecher.start(InetAddress.getLocalHost());
+    try {
+      TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath());
+      waitDownloadComplete(torrentManager, 7);
+      fail("Must fail, because file wasn't downloaded completely");
+    } catch (RuntimeException ignored) {
+    }
+  }
+
+  public void canStartAndStopClientTwice() throws Exception {
+    final ExecutorService es = Executors.newFixedThreadPool(DEFAULT_POOL_SIZE);
+    final ExecutorService validatorES = Executors.newFixedThreadPool(4);
+    final CommunicationManager communicationManager = new CommunicationManager(es, validatorES);
+    communicationManagerList.add(communicationManager);
+    try {
+      communicationManager.start(InetAddress.getLocalHost());
+      communicationManager.stop();
+      communicationManager.start(InetAddress.getLocalHost());
+      communicationManager.stop();
+    } finally {
+      es.shutdown();
+      validatorES.shutdown();
+    }
+  }
+
+  public void peer_dies_during_download() throws InterruptedException, IOException {
+    tracker.setAnnounceInterval(5);
+    final CommunicationManager seed1 = createClient();
+    final CommunicationManager seed2 = createClient();
+
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 240);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, tracker.getAnnounceURI(), "Test");
+
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seed1.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    seed1.start(InetAddress.getLocalHost());
+    seed1.setAnnounceInterval(5);
+    seed2.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    seed2.start(InetAddress.getLocalHost());
+    seed2.setAnnounceInterval(5);
+
+    CommunicationManager leecher = createClient();
+    leecher.start(InetAddress.getLocalHost());
+    leecher.setAnnounceInterval(5);
+    final ExecutorService service = Executors.newFixedThreadPool(1);
+    final Future<?> future = service.submit(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          Thread.sleep(5 * 1000);
+          seed1.removeTorrent(torrent.getHexInfoHash());
+          Thread.sleep(3 * 1000);
+          seed1.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+          seed2.removeTorrent(torrent.getHexInfoHash());
+        } catch (InterruptedException e) {
+          e.printStackTrace();
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+      }
+    });
+    try {
+      TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath());
+      waitDownloadComplete(torrentManager, 60);
+    } finally {
+      future.cancel(true);
+      service.shutdown();
+    }
+  }
+
+  public void torrentListenersPositiveTest() throws Exception {
+    tracker.setAcceptForeignTorrents(true);
+    CommunicationManager seeder = createClient();
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 24 + 1);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seeder.start(InetAddress.getLocalHost());
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    CommunicationManager leecher = createClient();
+    leecher.start(InetAddress.getLocalHost());
+    final AtomicInteger pieceLoadedInvocationCount = new AtomicInteger();
+    final AtomicInteger connectedInvocationCount = new AtomicInteger();
+    final Semaphore disconnectedLock = new Semaphore(0);
+    TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath());
+    final AtomicLong totalDownloaded = new AtomicLong();
+    torrentManager.addListener(new TorrentListenerWrapper() {
+      @Override
+      public void pieceDownloaded(PieceInformation pieceInformation, PeerInformation peerInformation) {
+        totalDownloaded.addAndGet(pieceInformation.getSize());
+        pieceLoadedInvocationCount.incrementAndGet();
+      }
+
+      @Override
+      public void peerConnected(PeerInformation peerInformation) {
+        connectedInvocationCount.incrementAndGet();
+      }
+
+      @Override
+      public void peerDisconnected(PeerInformation peerInformation) {
+        disconnectedLock.release();
+      }
+    });
+    waitDownloadComplete(torrentManager, 5);
+    assertEquals(pieceLoadedInvocationCount.get(), torrent.getPiecesCount());
+    assertEquals(connectedInvocationCount.get(), 1);
+    assertEquals(totalDownloaded.get(), dwnlFile.length());
+    if (!disconnectedLock.tryAcquire(10, TimeUnit.SECONDS)) {
+      fail("connection with seeder must be closed after download");
+    }
+  }
+
+  public void testClosingPieceStorageWhenDownloading() throws Exception {
+
+    tracker.setAcceptForeignTorrents(true);
+    final CommunicationManager seeder = createAndStartClient();
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 24);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+    final PieceStorage seederStorage = FullyPieceStorageFactory.INSTANCE.createStorage(torrent, new FileStorage(dwnlFile, 0, dwnlFile.length()));
+    seeder.addTorrent(new TorrentMetadataProvider() {
+      @NotNull
+      @Override
+      public TorrentMetadata getTorrentMetadata() {
+        return torrent;
+      }
+    }, seederStorage);
+
+    CommunicationManager leecher = createAndStartClient();
+
+    final PieceStorage leecherStorage = EmptyPieceStorageFactory.INSTANCE.createStorage(torrent, new FileStorage(tempFiles.createTempFile(), 0, dwnlFile.length()));
+    TorrentManager torrentManager = leecher.addTorrent(new TorrentMetadataProvider() {
+      @NotNull
+      @Override
+      public TorrentMetadata getTorrentMetadata() {
+        return torrent;
+      }
+    }, leecherStorage);
+
+    final AtomicReference<Throwable> exceptionHolder = new AtomicReference<Throwable>();
+    torrentManager.addListener(new TorrentListenerWrapper() {
+      @Override
+      public void pieceDownloaded(PieceInformation pieceInformation, PeerInformation peerInformation) {
+        try {
+          seederStorage.close();
+          leecherStorage.close();
+        } catch (IOException e) {
+          exceptionHolder.set(e);
+        }
+      }
+    });
+
+    waitDownloadComplete(torrentManager, 10);
+    Throwable throwable = exceptionHolder.get();
+    if (throwable != null) {
+      fail("", throwable);
+    }
+  }
+
+  public void testListenersWithBadSeeder() throws Exception {
+    tracker.setAcceptForeignTorrents(true);
+    CommunicationManager seeder = createClient();
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 240);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seeder.start(InetAddress.getLocalHost());
+    RandomAccessFile raf = new RandomAccessFile(dwnlFile, "rw");
+    //changing one byte in file. So one piece
+    try {
+      long pos = dwnlFile.length() / 2;
+      raf.seek(pos);
+      int oldByte = raf.read();
+      raf.seek(pos);
+      raf.write(oldByte + 1);
+    } finally {
+      raf.close();
+    }
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent(), FullyPieceStorageFactory.INSTANCE);
+    CommunicationManager leecher = createClient();
+    leecher.start(InetAddress.getLocalHost());
+    final AtomicInteger pieceLoadedInvocationCount = new AtomicInteger();
+    TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath());
+    torrentManager.addListener(new TorrentListenerWrapper() {
+      @Override
+      public void pieceDownloaded(PieceInformation pieceInformation, PeerInformation peerInformation) {
+        pieceLoadedInvocationCount.incrementAndGet();
+      }
+    });
+    try {
+      waitDownloadComplete(torrentManager, 15);
+      fail("Downloading must be failed because seeder doesn't have valid piece");
+    } catch (RuntimeException ignored) {
+    }
+    assertEquals(pieceLoadedInvocationCount.get(), torrent.getPiecesCount() - 1);
+  }
+
+  public void interrupt_download() throws IOException, InterruptedException {
+    tracker.setAcceptForeignTorrents(true);
+    final CommunicationManager seeder = createClient();
+    final File dwnlFile = tempFiles.createTempFile(513 * 1024 * 60);
+    final TorrentMetadata torrent = TorrentCreator.create(dwnlFile, null, tracker.getAnnounceURI(), "Test");
+
+    final File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seeder.start(InetAddress.getLocalHost());
+    seeder.addTorrent(torrentFile.getAbsolutePath(), dwnlFile.getParent());
+    final CommunicationManager leecher = createClient();
+    leecher.start(InetAddress.getLocalHost());
+    final AtomicBoolean interrupted = new AtomicBoolean();
+    final Thread th = new Thread() {
+      @Override
+      public void run() {
+        try {
+          TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(), tempFiles.createTempDir().getAbsolutePath());
+          waitDownloadComplete(torrentManager, 30);
+        } catch (ClosedByInterruptException e) {
+          interrupted.set(true);
+        } catch (IOException e) {
+          e.printStackTrace();
+        } catch (InterruptedException e) {
+          interrupted.set(true);
+        }
+      }
+    };
+    th.start();
+    Thread.sleep(100);
+    th.interrupt();
+    new WaitFor(10 * 1000) {
+      @Override
+      protected boolean condition() {
+        return !th.isAlive();
+      }
+    };
+
+    assertTrue(interrupted.get());
+  }
+
+  public void test_connect_to_unknown_host() throws InterruptedException, IOException {
+    final File torrent = new File("src/test/resources/torrents/file1.jar.torrent");
+    final TrackedTorrent tt = TrackedTorrent.load(torrent);
+    final CommunicationManager seeder = createAndStartClient();
+    final CommunicationManager leecher = createAndStartClient();
+    final TrackedTorrent announce = tracker.announce(tt);
+    final Random random = new Random();
+    final File leechFolder = tempFiles.createTempDir();
+
+    for (int i = 0; i < 40; i++) {
+      byte[] data = new byte[20];
+      random.nextBytes(data);
+      announce.addPeer(new TrackedPeer(tt, "my_unknown_and_unreachablehost" + i, 6881, ByteBuffer.wrap(data)));
+    }
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+    seeder.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath());
+    leecher.addTorrent(torrentFile.getAbsolutePath(), leechFolder.getAbsolutePath());
+    waitForFileInDir(leechFolder, "file1.jar");
+  }
+
+  public void test_seeding_does_not_change_file_modification_date() throws IOException, InterruptedException {
+    File srcFile = tempFiles.createTempFile(1024);
+    long time = srcFile.lastModified();
+
+    Thread.sleep(1000);
+
+    CommunicationManager seeder = createAndStartClient();
+
+    final TorrentMetadata torrent = TorrentCreator.create(srcFile, null, tracker.getAnnounceURI(), "Test");
+
+    File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    seeder.addTorrent(torrentFile.getAbsolutePath(), srcFile.getParent(), FullyPieceStorageFactory.INSTANCE);
+
+    final File downloadDir = tempFiles.createTempDir();
+    CommunicationManager leech = createClient();
+    leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath());
+
+    leech.start(InetAddress.getLocalHost());
+
+    waitForFileInDir(downloadDir, srcFile.getName());
+
+    assertEquals(time, srcFile.lastModified());
+  }
+
+  private void downloadAndStop(TorrentMetadata torrent, long timeout, final CommunicationManager leech) throws IOException {
+    final File tempDir = tempFiles.createTempDir();
+    File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    leech.addTorrent(torrentFile.getAbsolutePath(), tempDir.getAbsolutePath());
+    leech.start(InetAddress.getLocalHost());
+
+    waitForFileInDir(tempDir, torrent.getFiles().get(0).getRelativePathAsString());
+
+    leech.stop();
+  }
+
+  private void validateMultipleClientsResults(final List<CommunicationManager> clientsList,
+                                              MessageDigest md5,
+                                              final File baseFile,
+                                              String baseMD5,
+                                              final String hash,
+                                              final List<File> targetFiles) throws IOException {
+
+    final WaitFor waitFor = new WaitFor(75 * 1000) {
+      @Override
+      protected boolean condition() {
+        boolean retval = true;
+        for (int i = 0; i < clientsList.size(); i++) {
+          if (!retval) return false;
+          File target = targetFiles.get(i);
+          retval = target.isFile();
+        }
+        return retval;
+      }
+    };
+
+    assertTrue(waitFor.isMyResult(), "All seeders didn't get their files");
+    // check file contents here:
+    for (int i = 0; i < clientsList.size(); i++) {
+      final LoadedTorrent torrent = communicationManagerList.get(i).getTorrentsStorage().getLoadedTorrent(hash);
+      final File file = targetFiles.get(i);
+      assertEquals(baseMD5, getFileMD5(file, md5), String.format("MD5 hash is invalid. C:%s, O:%s ",
+              file.getAbsolutePath(), baseFile.getAbsolutePath()));
+    }
+  }
+
+  public void testManySeeders() throws Exception {
+    File artifact = tempFiles.createTempFile(256 * 1024 * 1024);
+    int seedersCount = 15;
+    TorrentMetadata torrent = TorrentCreator.create(artifact, this.tracker.getAnnounceURI(), "test");
+    File torrentFile = tempFiles.createTempFile();
+    saveTorrent(torrent, torrentFile);
+    ServerChannelRegister serverChannelRegister = new FirstAvailableChannel(6881, 10000);
+    for (int i = 0; i < seedersCount; i++) {
+      CommunicationManager seeder = createClient();
+      seeder.addTorrent(torrentFile.getAbsolutePath(), artifact.getParent(), FullyPieceStorageFactory.INSTANCE);
+      seeder.start(new InetAddress[]{InetAddress.getLocalHost()},
+              Constants.DEFAULT_ANNOUNCE_INTERVAL_SEC,
+              null,
+              new SelectorFactoryImpl(),
+              serverChannelRegister);
+    }
+
+    CommunicationManager leecher = createClient();
+    leecher.start(new InetAddress[]{InetAddress.getLocalHost()},
+            Constants.DEFAULT_ANNOUNCE_INTERVAL_SEC,
+            null,
+            new SelectorFactoryImpl(),
+            serverChannelRegister);
+    TorrentManager torrentManager = leecher.addTorrent(torrentFile.getAbsolutePath(),
+            tempFiles.createTempDir().getAbsolutePath(),
+            EmptyPieceStorageFactory.INSTANCE);
+    waitDownloadComplete(torrentManager, 60);
+  }
+
+  private String createMultipleSeedersWithDifferentPieces(File baseFile, int piecesCount, int pieceSize, int numSeeders,
+                                                          List<CommunicationManager> communicationManagerList, List<File> targetFiles) throws IOException, InterruptedException, URISyntaxException {
+
+    List<byte[]> piecesList = new ArrayList<byte[]>(piecesCount);
+    FileInputStream fin = new FileInputStream(baseFile);
+    for (int i = 0; i < piecesCount; i++) {
+      byte[] piece = new byte[pieceSize];
+      fin.read(piece);
+      piecesList.add(piece);
+    }
+    fin.close();
+
+    final long torrentFileLength = baseFile.length();
+    TorrentMetadata torrent = TorrentCreator.create(baseFile, null, this.tracker.getAnnounceURI(), null, "Test", pieceSize);
+    File torrentFile = new File(baseFile.getParentFile(), baseFile.getName() + ".torrent");
+    saveTorrent(torrent, torrentFile);
+
+
+    for (int i = 0; i < numSeeders; i++) {
+      final File baseDir = tempFiles.createTempDir();
+      targetFiles.add(new File(baseDir, baseFile.getName()));
+      final File seederPiecesFile = new File(baseDir, baseFile.getName());
+      RandomAccessFile raf = new RandomAccessFile(seederPiecesFile, "rw");
+      raf.setLength(torrentFileLength);
+      for (int pieceIdx = i; pieceIdx < piecesCount; pieceIdx += numSeeders) {
+        raf.seek(pieceIdx * pieceSize);
+        raf.write(piecesList.get(pieceIdx));
+      }
+      CommunicationManager communicationManager = createClient(" communicationManager idx " + i);
+      communicationManagerList.add(communicationManager);
+      communicationManager.addTorrent(torrentFile.getAbsolutePath(), baseDir.getAbsolutePath());
+      communicationManager.start(InetAddress.getLocalHost());
+    }
+    return torrent.getHexInfoHash();
+  }
+
+  private String getFileMD5(File file, MessageDigest digest) throws IOException {
+    DigestInputStream dIn = new DigestInputStream(new FileInputStream(file), digest);
+    while (dIn.read() >= 0) ;
+    return dIn.getMessageDigest().toString();
+  }
+
+  private void waitForFileInDir(final File downloadDir, final String fileName) {
+    new WaitFor() {
+      @Override
+      protected boolean condition() {
+        return new File(downloadDir, fileName).isFile();
+      }
+    };
+
+    assertTrue(new File(downloadDir, fileName).isFile());
+  }
+
+
+  @AfterMethod
+  protected void tearDown() throws Exception {
+    for (CommunicationManager communicationManager : communicationManagerList) {
+      communicationManager.stop();
+    }
+    stopTracker();
+    tempFiles.cleanup();
+  }
+
+  private void startTracker() throws IOException {
+    int port = 6969;
+    this.tracker = new Tracker(port, "http://" + InetAddress.getLocalHost().getHostAddress() + ":" + port + "" + ANNOUNCE_URL);
+    tracker.setAnnounceInterval(5);
+    this.tracker.start(true);
+  }
+
+  private CommunicationManager createAndStartClient() throws IOException, InterruptedException {
+    CommunicationManager communicationManager = createClient();
+    communicationManager.start(InetAddress.getLocalHost());
+    return communicationManager;
+  }
+
+  private CommunicationManager createClient(String name) {
+    final CommunicationManager communicationManager = communicationManagerFactory.getClient(name);
+    communicationManagerList.add(communicationManager);
+    return communicationManager;
+  }
+
+  private CommunicationManager createClient() {
+    return createClient("");
+  }
+
+  private void stopTracker() {
+    this.tracker.stop();
+  }
+
+  private void assertFilesEqual(File f1, File f2) throws IOException {
+    assertEquals(f1.length(), f2.length(), "Files sizes differ");
+    Checksum c1 = FileUtils.checksum(f1, new CRC32());
+    Checksum c2 = FileUtils.checksum(f2, new CRC32());
+    assertEquals(c1.getValue(), c2.getValue());
+  }
+}
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/client/announce/TrackerClientTest.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/client/announce/TrackerClientTest.java
new file mode 100644
index 0000000..8a15b59
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/client/announce/TrackerClientTest.java
@@ -0,0 +1,104 @@
+package com.turn.ttorrent.client.announce;
+
+import com.turn.ttorrent.Utils;
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentUtils;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import com.turn.ttorrent.tracker.Tracker;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.mockito.ArgumentMatchers;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.Mockito.*;
+
+@Test
+public class TrackerClientTest {
+
+  private Tracker tracker;
+
+  public TrackerClientTest() {
+    if (Logger.getRootLogger().getAllAppenders().hasMoreElements())
+      return;
+    BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("[%d{MMdd HH:mm:ss,SSS}] %6p - %20.20c - %m %n")));
+    Logger.getRootLogger().setLevel(Utils.getLogLevel());
+  }
+
+  @BeforeMethod
+  protected void setUp() throws Exception {
+    startTracker();
+  }
+
+
+  @Test
+  public void multiAnnounceTest() throws AnnounceException, ConnectException {
+    List<Peer> peers = Collections.singletonList(new Peer(new InetSocketAddress("127.0.0.1", 6881), ByteBuffer.allocate(1)));
+    final URI trackerURI = URI.create("http://localhost:6969/announce");
+    TrackerClient client = new HTTPTrackerClient(peers, trackerURI);
+
+    final AnnounceableInformation firstTorrent = getMockedTorrent(new byte[]{1, 2, 3, 4});
+    final AnnounceableInformation secondTorrent = getMockedTorrent(new byte[]{1, 3, 3, 2});
+    List<AnnounceableInformation> torrents = Arrays.asList(firstTorrent, secondTorrent);
+
+    client.multiAnnounce(AnnounceRequestMessage.RequestEvent.STARTED, true, torrents, peers);
+
+    peers = Collections.singletonList(new Peer(new InetSocketAddress("127.0.0.1", 6882), ByteBuffer.allocate(1)));
+    client.multiAnnounce(AnnounceRequestMessage.RequestEvent.STARTED, true, torrents, peers);
+
+    List<Peer> leecher = Collections.singletonList(new Peer(new InetSocketAddress("127.0.0.1", 6885), ByteBuffer.allocate(1)));
+    final AnnounceableInformation firstTorrentLeech = getMockedTorrent(new byte[]{1, 2, 3, 4});
+    final AnnounceableInformation secondTorrentLeech = getMockedTorrent(new byte[]{1, 3, 3, 2});
+    when(firstTorrentLeech.getLeft()).thenReturn(10L);
+    when(secondTorrentLeech.getLeft()).thenReturn(10L);
+
+    AnnounceResponseListener listener = mock(AnnounceResponseListener.class);
+
+    client.register(listener);
+    client.multiAnnounce(AnnounceRequestMessage.RequestEvent.STARTED, false,
+            Arrays.asList(secondTorrentLeech, firstTorrentLeech), leecher);
+
+    verify(listener, times(2)).handleAnnounceResponse(anyInt(), anyInt(), anyInt(), anyString());
+    verify(listener, times(2)).handleDiscoveredPeers(ArgumentMatchers.<Peer>anyList(), anyString());
+
+  }
+
+  private AnnounceableInformation getMockedTorrent(byte[] hash) {
+    final AnnounceableInformation result = mock(AnnounceableInformation.class);
+    when(result.getLeft()).thenReturn(0L);
+    when(result.getDownloaded()).thenReturn(0L);
+    when(result.getUploaded()).thenReturn(0L);
+    when(result.getInfoHash()).thenReturn(hash);
+    when(result.getHexInfoHash()).thenReturn(TorrentUtils.byteArrayToHexString(hash));
+    return result;
+  }
+
+  private void startTracker() throws IOException {
+    this.tracker = new Tracker(6969);
+    tracker.setAnnounceInterval(5);
+    tracker.setPeerCollectorExpireTimeout(10);
+    this.tracker.start(true);
+  }
+
+  private void stopTracker() {
+    this.tracker.stop();
+  }
+
+  @AfterMethod
+  protected void tearDown() throws Exception {
+    stopTracker();
+  }
+}
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/common/TorrentTest.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/common/TorrentTest.java
new file mode 100644
index 0000000..679c52f
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/common/TorrentTest.java
@@ -0,0 +1,77 @@
+package com.turn.ttorrent.common;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.io.FileUtils;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.testng.Assert.*;
+
+@Test
+public class TorrentTest {
+
+  public void test_create_torrent() throws URISyntaxException, IOException, InterruptedException {
+    URI announceURI = new URI("http://localhost:6969/announce");
+    String createdBy = "Test";
+    TorrentMetadata t = TorrentCreator.create(new File("src/test/resources/parentFiles/file1.jar"), announceURI, createdBy);
+    assertEquals(createdBy, t.getCreatedBy().get());
+    assertEquals(announceURI.toString(), t.getAnnounce());
+  }
+
+  public void load_torrent_created_by_utorrent() throws IOException {
+    TorrentMetadata t = new TorrentParser().parseFromFile(new File("src/test/resources/torrents/file1.jar.torrent"));
+    assertEquals("http://localhost:6969/announce", t.getAnnounce());
+    assertEquals("B92D38046C76D73948E14C42DF992CAF25489D08", t.getHexInfoHash());
+    assertEquals("uTorrent/3130", t.getCreatedBy().get());
+  }
+
+  public void torrent_from_multiple_files() throws URISyntaxException, InterruptedException, IOException {
+    URI announceURI = new URI("http://localhost:6969/announce");
+    String createdBy = "Test2";
+    final File parentDir = new File("src/test/resources/parentFiles/parentDir");
+    final long creationTimeSecs = 1376051000;
+    final String[] fileNames = new String[]
+            {"AccuRevCommon.jar",
+                    "commons-io-cio2.5_3.jar",
+                    "commons-io-cio2.5_3.jar.link",
+                    "inDir/application.wadl",
+                    "storage.version"};
+    final List<File> files = new ArrayList<File>();
+    for (String fileName : fileNames) {
+      files.add(new File(parentDir, fileName));
+    }
+    TorrentMetadata createdTorrent = TorrentCreator.create(parentDir, files, announceURI, null, createdBy, creationTimeSecs, TorrentCreator.DEFAULT_PIECE_LENGTH);
+    File torrentFileWin = new File("src/test/resources/torrents/parentDir.win.torrent");
+    File torrentFileLinux = new File("src/test/resources/torrents/parentDir.linux.torrent");
+    final byte[] expectedBytesWin = FileUtils.readFileToByteArray(torrentFileWin);
+    final byte[] expectedBytesLinux = FileUtils.readFileToByteArray(torrentFileLinux);
+    final byte[] actualBytes = new TorrentSerializer().serialize(createdTorrent);
+
+    assertTrue(Hex.encodeHexString(expectedBytesWin).equals(Hex.encodeHexString(actualBytes)) || Hex.encodeHexString(expectedBytesLinux).equals(Hex.encodeHexString(actualBytes)));
+  }
+
+  public void testFilenames() throws IOException {
+    File torrentFile = new File("src/test/resources/torrents/parentDir.win.torrent");
+    TorrentMetadata t2 = new TorrentParser().parseFromFile(torrentFile);
+    final List<TorrentFile> tmpFileNames = t2.getFiles();
+    final List<String> normalizedFilenames = new ArrayList<String>(tmpFileNames.size());
+    for (TorrentFile torrentFileInfo : tmpFileNames) {
+      normalizedFilenames.add(t2.getDirectoryName() + "/" + torrentFileInfo.getRelativePathAsString().replaceAll("\\\\", "/"));
+    }
+    String[] expectedFilenames = new String[]
+            {"parentDir/AccuRevCommon.jar",
+                    "parentDir/commons-io-cio2.5_3.jar",
+                    "parentDir/commons-io-cio2.5_3.jar.link",
+                    "parentDir/inDir/application.wadl",
+                    "parentDir/storage.version"};
+    assertEqualsNoOrder(normalizedFilenames.toArray(new String[normalizedFilenames.size()]), expectedFilenames);
+    System.out.println();
+  }
+
+}
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerAnnounceTest.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerAnnounceTest.java
new file mode 100644
index 0000000..4aad8c2
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerAnnounceTest.java
@@ -0,0 +1,38 @@
+package com.turn.ttorrent.tracker;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+
+import static com.turn.ttorrent.tracker.TrackerUtils.loadTorrent;
+import static org.testng.Assert.assertEquals;
+
+@Test
+public class TrackerAnnounceTest {
+
+  private Tracker tracker;
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    this.tracker = new Tracker(6969);
+    tracker.setAnnounceInterval(5);
+    tracker.setPeerCollectorExpireTimeout(10);
+    this.tracker.start(false);
+  }
+
+  public void test_announce() throws IOException {
+
+    assertEquals(0, this.tracker.getTrackedTorrents().size());
+
+    this.tracker.announce(loadTorrent("file1.jar.torrent"));
+
+    assertEquals(1, this.tracker.getTrackedTorrents().size());
+  }
+
+  @AfterMethod
+  public void tearDown() throws Exception {
+    this.tracker.stop();
+  }
+}
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerTest.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerTest.java
new file mode 100644
index 0000000..39d6802
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerTest.java
@@ -0,0 +1,422 @@
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.CommunicationManagerFactory;
+import com.turn.ttorrent.TempFiles;
+import com.turn.ttorrent.Utils;
+import com.turn.ttorrent.WaitFor;
+import com.turn.ttorrent.client.CommunicationManager;
+import com.turn.ttorrent.client.SharedTorrent;
+import com.turn.ttorrent.client.storage.FairPieceStorageFactory;
+import com.turn.ttorrent.client.storage.FileCollectionStorage;
+import com.turn.ttorrent.common.*;
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.*;
+import java.util.*;
+import java.util.zip.CRC32;
+import java.util.zip.Checksum;
+
+import static com.turn.ttorrent.tracker.Tracker.ANNOUNCE_URL;
+import static com.turn.ttorrent.tracker.TrackerUtils.TEST_RESOURCES;
+import static org.testng.Assert.*;
+
+@Test
+public class TrackerTest {
+
+  private Tracker tracker;
+  private TempFiles tempFiles;
+  //  private String myLogfile;
+  private List<CommunicationManager> communicationManagerList = new ArrayList<CommunicationManager>();
+
+  private final CommunicationManagerFactory communicationManagerFactory;
+
+
+  public TrackerTest() {
+    communicationManagerFactory = new CommunicationManagerFactory();
+    if (Logger.getRootLogger().getAllAppenders().hasMoreElements())
+      return;
+    BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("[%d{MMdd HH:mm:ss,SSS}] %6p - %20.20c - %m %n")));
+    Logger.getRootLogger().setLevel(Utils.getLogLevel());
+  }
+
+  @BeforeMethod
+  protected void setUp() throws Exception {
+    tempFiles = new TempFiles();
+    startTracker();
+  }
+
+  public void test_tracker_all_ports() throws IOException {
+    final int port = tracker.getAnnounceURI().getPort();
+    final Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
+    while (e.hasMoreElements()) {
+      final NetworkInterface ni = e.nextElement();
+      final Enumeration<InetAddress> addresses = ni.getInetAddresses();
+      while (addresses.hasMoreElements()) {
+        final InetAddress addr = addresses.nextElement();
+        try {
+          Socket s = new Socket(addr, port);
+          s.close();
+        } catch (Exception ex) {
+          if (System.getProperty("java.version").startsWith("1.7.") || addr instanceof Inet4Address) {
+            fail("Unable to connect to " + addr, ex);
+          }
+        }
+      }
+
+    }
+  }
+
+  public void testPeerWithManyInterfaces() throws Exception {
+    List<InetAddress> selfAddresses = new ArrayList<InetAddress>();
+    final Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
+    while (networkInterfaces.hasMoreElements()) {
+      NetworkInterface ni = networkInterfaces.nextElement();
+      final Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
+      while (inetAddresses.hasMoreElements()) {
+        InetAddress inetAddress = inetAddresses.nextElement();
+        if (inetAddress instanceof Inet6Address) continue;// ignore IPv6 addresses
+
+        selfAddresses.add(inetAddress);
+      }
+    }
+
+    final InetAddress[] inetAddresses = selfAddresses.toArray(new InetAddress[selfAddresses.size()]);
+    CommunicationManager seeder = createCommunicationManager();
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+    final String hexInfoHash = seeder.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath()).getHexInfoHash();
+    seeder.start(inetAddresses);
+    final WaitFor waitFor = new WaitFor(10000) {
+      @Override
+      protected boolean condition() {
+        final TrackedTorrent trackedTorrent = tracker.getTrackedTorrent(hexInfoHash);
+        return trackedTorrent != null && trackedTorrent.getPeers().size() >= inetAddresses.length;
+      }
+    };
+
+    assertTrue(waitFor.isMyResult());
+
+    final TrackedTorrent trackedTorrent = tracker.getTrackedTorrent(hexInfoHash);
+
+    Set<String> expectedIps = new HashSet<String>();
+    for (InetAddress inetAddress : inetAddresses) {
+      expectedIps.add(inetAddress.getHostAddress());
+    }
+    Set<String> actualIps = new HashSet<String>();
+    for (TrackedPeer peer : trackedTorrent.getPeers().values()) {
+      actualIps.add(peer.getIp());
+    }
+
+    assertEquals(actualIps, expectedIps);
+    assertEquals(inetAddresses.length, actualIps.size());
+
+  }
+
+  public void test_share_and_download() throws IOException, InterruptedException {
+    final TrackedTorrent tt = this.tracker.announce(loadTorrent("file1.jar.torrent"));
+    assertEquals(0, tt.getPeers().size());
+
+    CommunicationManager seeder = createCommunicationManager();
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+    seeder.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath());
+
+    assertEquals(tt.getHexInfoHash(), seeder.getTorrentsStorage().announceableTorrents().iterator().next().getHexInfoHash());
+
+    final File downloadDir = tempFiles.createTempDir();
+    CommunicationManager leech = createCommunicationManager();
+    leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath());
+
+    try {
+      seeder.start(InetAddress.getLocalHost());
+
+      leech.start(InetAddress.getLocalHost());
+
+      waitForFileInDir(downloadDir, "file1.jar");
+      assertFilesEqual(new File(TEST_RESOURCES + "/parentFiles/file1.jar"), new File(downloadDir, "file1.jar"));
+    } finally {
+      leech.stop();
+      seeder.stop();
+    }
+  }
+
+  public void tracker_accepts_torrent_from_seeder() throws IOException, InterruptedException {
+    this.tracker.setAcceptForeignTorrents(true);
+    CommunicationManager seeder = createCommunicationManager();
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+    seeder.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath());
+
+    try {
+      seeder.start(InetAddress.getLocalHost());
+
+      waitForSeeder(seeder.getTorrentsStorage().announceableTorrents().iterator().next().getInfoHash());
+
+      Collection<TrackedTorrent> trackedTorrents = this.tracker.getTrackedTorrents();
+      assertEquals(1, trackedTorrents.size());
+
+      TrackedTorrent trackedTorrent = trackedTorrents.iterator().next();
+      Map<PeerUID, TrackedPeer> peers = trackedTorrent.getPeers();
+      assertEquals(1, peers.size());
+      assertTrue(peers.values().iterator().next().isCompleted()); // seed
+      assertEquals(1, trackedTorrent.seeders());
+      assertEquals(0, trackedTorrent.leechers());
+    } finally {
+      seeder.stop();
+    }
+  }
+
+  public void tracker_accepts_torrent_from_leech() throws IOException, InterruptedException {
+    this.tracker.setAcceptForeignTorrents(true);
+
+    final File downloadDir = tempFiles.createTempDir();
+    CommunicationManager leech = createCommunicationManager();
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath());
+
+    try {
+      leech.start(InetAddress.getLocalHost());
+
+      new WaitFor() {
+        @Override
+        protected boolean condition() {
+          for (TrackedTorrent tt : tracker.getTrackedTorrents()) {
+            if (tt.getPeers().size() == 1) return true;
+          }
+
+          return false;
+        }
+      };
+
+      Collection<TrackedTorrent> trackedTorrents = this.tracker.getTrackedTorrents();
+      assertEquals(1, trackedTorrents.size());
+
+      TrackedTorrent trackedTorrent = trackedTorrents.iterator().next();
+      Map<PeerUID, TrackedPeer> peers = trackedTorrent.getPeers();
+      assertEquals(1, peers.size());
+      assertFalse(peers.values().iterator().next().isCompleted()); // leech
+      assertEquals(0, trackedTorrent.seeders());
+      assertEquals(1, trackedTorrent.leechers());
+    } finally {
+      leech.stop();
+    }
+  }
+
+  public void tracker_removes_peer_after_peer_shutdown() throws IOException, InterruptedException {
+    tracker.setAcceptForeignTorrents(true);
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+
+    final CommunicationManager c1 = createCommunicationManager();
+    c1.start(InetAddress.getLocalHost());
+    c1.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath());
+
+    final CommunicationManager c2 = createCommunicationManager();
+    c2.start(InetAddress.getLocalHost());
+    c2.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath());
+
+    new WaitFor(10 * 1000) {
+      @Override
+      protected boolean condition() {
+        return tracker.getTrackedTorrents().size() == 1;
+      }
+    };
+
+    final TrackedTorrent tt = tracker.getTrackedTorrents().iterator().next();
+
+    new WaitFor(10 * 1000) {
+      @Override
+      protected boolean condition() {
+        return tt.getPeers().size() == 2;
+      }
+    };
+
+    final InetSocketAddress c1Address = new InetSocketAddress(InetAddress.getLocalHost(), c1.getConnectionManager().getBindPort());
+    final InetSocketAddress c2Address = new InetSocketAddress(InetAddress.getLocalHost(), c2.getConnectionManager().getBindPort());
+    assertTrue(tt.getPeers().containsKey(new PeerUID(c1Address, tt.getHexInfoHash())));
+    assertTrue(tt.getPeers().containsKey(new PeerUID(c2Address, tt.getHexInfoHash())));
+
+    c2.stop();
+    new WaitFor(30 * 1000) {
+
+      @Override
+      protected boolean condition() {
+        return tt.getPeers().size() == 1;
+      }
+    };
+    assertTrue(tt.getPeers().containsKey(new PeerUID(c1Address, tt.getHexInfoHash())));
+    assertFalse(tt.getPeers().containsKey(new PeerUID(c2Address, tt.getHexInfoHash())));
+  }
+
+  public void tracker_removes_peer_after_timeout() throws IOException, InterruptedException {
+    tracker.setAcceptForeignTorrents(true);
+    tracker.stop();
+    tracker.start(true);
+    final SharedTorrent torrent = completeTorrent("file1.jar.torrent");
+    tracker.setPeerCollectorExpireTimeout(5);
+
+    int peerPort = 6885;
+    String peerHost = InetAddress.getLocalHost().getHostAddress();
+    final String announceUrlC1 = "http://localhost:6969/announce?info_hash=%B9-8%04lv%D79H%E1LB%DF%99%2C%AF%25H%9D%08&peer_id=-TO0042-97ec308c9637&" +
+            "port=" + peerPort + "&uploaded=0&downloaded=0&left=0&compact=1&no_peer_id=0&ip=" + peerHost;
+
+    try {
+      final URLConnection connection = new URL(announceUrlC1).openConnection();
+      connection.getInputStream().close();
+    } catch (Exception e) {
+      fail("", e);
+    }
+
+    final CommunicationManager c2 = createCommunicationManager();
+    c2.setAnnounceInterval(120);
+    c2.start(InetAddress.getLocalHost());
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+    c2.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath());
+
+    final TrackedTorrent tt = tracker.getTrackedTorrent(torrent.getHexInfoHash());
+    new WaitFor(10 * 1000) {
+      @Override
+      protected boolean condition() {
+
+        return tt.getPeers().size() == 2;
+      }
+    };
+
+    final InetSocketAddress c1Address = new InetSocketAddress(peerHost, peerPort);
+    final InetSocketAddress c2Address = new InetSocketAddress(InetAddress.getLocalHost(), c2.getConnectionManager().getBindPort());
+    assertTrue(tt.getPeers().containsKey(new PeerUID(c1Address, tt.getHexInfoHash())));
+    assertTrue(tt.getPeers().containsKey(new PeerUID(c2Address, tt.getHexInfoHash())));
+
+    new WaitFor(30 * 1000) {
+
+      @Override
+      protected boolean condition() {
+        try {
+          final URLConnection connection = new URL(announceUrlC1).openConnection();
+          connection.getInputStream().close();
+        } catch (Exception e) {
+          e.printStackTrace();
+        }
+        return tt.getPeers().size() == 1;
+      }
+    };
+    assertEquals(tt.getPeers().size(), 1);
+    assertTrue(tt.getPeers().containsKey(new PeerUID(c1Address, tt.getHexInfoHash())));
+    assertFalse(tt.getPeers().containsKey(new PeerUID(c2Address, tt.getHexInfoHash())));
+  }
+
+  //  @Test(invocationCount = 50)
+  public void tracker_accepts_torrent_from_seeder_plus_leech() throws IOException, InterruptedException {
+    this.tracker.setAcceptForeignTorrents(true);
+    assertEquals(0, this.tracker.getTrackedTorrents().size());
+
+    CommunicationManager seeder = createCommunicationManager();
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", "file1.jar.torrent");
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+    seeder.addTorrent(torrentFile.getAbsolutePath(), parentFiles.getAbsolutePath());
+
+    final File downloadDir = tempFiles.createTempDir();
+    CommunicationManager leech = createCommunicationManager();
+    leech.addTorrent(torrentFile.getAbsolutePath(), downloadDir.getAbsolutePath());
+
+    try {
+      seeder.start(InetAddress.getLocalHost());
+      leech.start(InetAddress.getLocalHost());
+
+      waitForFileInDir(downloadDir, "file1.jar");
+    } finally {
+      seeder.stop();
+      leech.stop();
+    }
+  }
+
+  private TrackedTorrent loadTorrent(String name) throws IOException {
+    TorrentMetadata torrentMetadata = new TorrentParser().parseFromFile(new File(TEST_RESOURCES + "/torrents", name));
+    return new TrackedTorrent(torrentMetadata.getInfoHash());
+  }
+
+  private void startTracker() throws IOException {
+    int port = 6969;
+    this.tracker = new Tracker(port, "http://" + InetAddress.getLocalHost().getHostAddress() + ":" + port + "" + ANNOUNCE_URL);
+    tracker.setAnnounceInterval(5);
+    tracker.setPeerCollectorExpireTimeout(10);
+    this.tracker.start(true);
+  }
+
+  private void stopTracker() {
+    this.tracker.stop();
+  }
+
+  @AfterMethod
+  protected void tearDown() throws Exception {
+    for (CommunicationManager communicationManager : communicationManagerList) {
+      communicationManager.stop();
+    }
+    stopTracker();
+    tempFiles.cleanup();
+  }
+
+  private CommunicationManager createCommunicationManager() {
+    final CommunicationManager communicationManager = communicationManagerFactory.getClient("");
+    communicationManagerList.add(communicationManager);
+    return communicationManager;
+  }
+
+  private void waitForFileInDir(final File downloadDir, final String fileName) {
+    new WaitFor() {
+      @Override
+      protected boolean condition() {
+        return new File(downloadDir, fileName).isFile();
+      }
+    };
+
+    assertTrue(new File(downloadDir, fileName).isFile());
+  }
+
+  private SharedTorrent completeTorrent(String name) throws IOException {
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", name);
+    File parentFiles = new File(TEST_RESOURCES + "/parentFiles");
+    TorrentMetadata torrentMetadata = new TorrentParser().parseFromFile(torrentFile);
+    return SharedTorrent.fromFile(torrentFile,
+            FairPieceStorageFactory.INSTANCE.createStorage(torrentMetadata, FileCollectionStorage.create(torrentMetadata, parentFiles)),
+            new TorrentStatistic());
+  }
+
+  private SharedTorrent incompleteTorrent(String name, File destDir) throws IOException {
+    File torrentFile = new File(TEST_RESOURCES + "/torrents", name);
+    TorrentMetadata torrentMetadata = new TorrentParser().parseFromFile(torrentFile);
+    return SharedTorrent.fromFile(torrentFile,
+              FairPieceStorageFactory.INSTANCE.createStorage(torrentMetadata, FileCollectionStorage.create(torrentMetadata, destDir)),
+              new TorrentStatistic());
+  }
+
+  private void waitForSeeder(final byte[] torrentHash) {
+    new WaitFor() {
+      @Override
+      protected boolean condition() {
+        for (TrackedTorrent tt : tracker.getTrackedTorrents()) {
+          if (tt.seeders() == 1 && tt.getHexInfoHash().equals(TorrentUtils.byteArrayToHexString(torrentHash))) return true;
+        }
+
+        return false;
+      }
+    };
+  }
+
+  private void assertFilesEqual(File f1, File f2) throws IOException {
+    assertEquals(f1.length(), f2.length(), "Files sizes differ");
+    Checksum c1 = FileUtils.checksum(f1, new CRC32());
+    Checksum c2 = FileUtils.checksum(f2, new CRC32());
+    assertEquals(c1.getValue(), c2.getValue());
+  }
+}
diff --git a/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerUtils.java b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerUtils.java
new file mode 100644
index 0000000..e7a7ecb
--- /dev/null
+++ b/ttorrent-master/tests/src/test/java/com/turn/ttorrent/tracker/TrackerUtils.java
@@ -0,0 +1,18 @@
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentParser;
+
+import java.io.File;
+import java.io.IOException;
+
+public class TrackerUtils {
+
+  public static final String TEST_RESOURCES = "src/test/resources";
+
+  public static TrackedTorrent loadTorrent(String name) throws IOException {
+    TorrentMetadata torrentMetadata = new TorrentParser().parseFromFile(new File(TEST_RESOURCES + "/torrents", name));
+    return new TrackedTorrent(torrentMetadata.getInfoHash());
+  }
+
+}
diff --git a/ttorrent-master/tests/src/test/resources/parentFiles/file1.jar b/ttorrent-master/tests/src/test/resources/parentFiles/file1.jar
new file mode 100644
index 0000000..add2577
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/parentFiles/file1.jar
Binary files differ
diff --git a/ttorrent-master/tests/src/test/resources/parentFiles/file2.jar b/ttorrent-master/tests/src/test/resources/parentFiles/file2.jar
new file mode 100644
index 0000000..95d8e21
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/parentFiles/file2.jar
Binary files differ
diff --git a/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/AccuRevCommon.jar b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/AccuRevCommon.jar
new file mode 100644
index 0000000..4c5db25
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/AccuRevCommon.jar
Binary files differ
diff --git a/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/commons-io-cio2.5_3.jar b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/commons-io-cio2.5_3.jar
new file mode 100644
index 0000000..d896ec0
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/commons-io-cio2.5_3.jar
Binary files differ
diff --git a/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/commons-io-cio2.5_3.jar.link b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/commons-io-cio2.5_3.jar.link
new file mode 100644
index 0000000..6af92ff
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/commons-io-cio2.5_3.jar.link
@@ -0,0 +1,2 @@
+E:\DevData\TeamcityBuilds\8.1\TeamCity-29117\Data\system\artifacts\CommonsIo\one\7\commons-io-cio2.5_3.jar
+E:\DevData\TeamcityBuilds\8.1\TeamCity-29117\Data\system\artifacts\CommonsIo\one\7\.teamcity\torrents\commons-io-cio2.5_3.jar.torrent
\ No newline at end of file
diff --git a/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/inDir/application.wadl b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/inDir/application.wadl
new file mode 100644
index 0000000..f8655d5
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/inDir/application.wadl
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ns2:application xmlns:ns2="http://wadl.dev.java.net/2009/02"><ns2:doc xmlns:jersey="http://jersey.java.net/" jersey:generatedBy="Jersey: 1.16 11/28/2012 02:09 PM"/><ns2:doc title="TeamCity REST API" xml:lang="en">
+    See also http://confluence.jetbrains.net/display/TW/REST+API+Plugin
+  </ns2:doc><ns2:grammars><ns2:include href="application.wadl/xsd1.xsd"><ns2:doc title="Generated" xml:lang="en"/></ns2:include><ns2:include href="application.wadl/xsd0.xsd"><ns2:doc title="Generated" xml:lang="en"/></ns2:include></ns2:grammars><ns2:resources base="http://buildserver.labs.intellij.net/"><ns2:resource path="/app/rest/projects"><ns2:method id="serveProjects" name="GET"><ns2:response><ns2:representation element="projects" mediaType="application/xml"/><ns2:representation element="projects" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="createProject" name="POST"><ns2:request><ns2:representation element="newProjectDescription" mediaType="application/xml"/><ns2:representation element="newProjectDescription" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="project" mediaType="application/xml"/><ns2:representation element="project" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="createEmptyProject" name="POST"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation element="project" mediaType="application/xml"/><ns2:representation element="project" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{projectLocator}/templates"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:method id="createBuildTypeTemplate" name="POST"><ns2:doc>Creates a new build configuration template by copying existing one.</ns2:doc><ns2:request><ns2:representation element="newBuildTypeDescription" mediaType="application/xml"/><ns2:representation element="newBuildTypeDescription" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="buildType" mediaType="application/xml"/><ns2:representation element="buildType" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="serveTemplatesInProject" name="GET"><ns2:response><ns2:representation element="buildTypes" mediaType="application/xml"/><ns2:representation element="buildTypes" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="createEmptyBuildTypeTemplate" name="POST"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation element="buildType" mediaType="application/xml"/><ns2:representation element="buildType" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/parameters/{name}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/><ns2:method id="putParameter" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="serveParameter" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="deleteParameter" name="DELETE"/></ns2:resource><ns2:resource path="/{projectLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:method id="serveProject" name="GET"><ns2:response><ns2:representation element="project" mediaType="application/xml"/><ns2:representation element="project" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteProject" name="DELETE"/></ns2:resource><ns2:resource path="/{projectLocator}/parentProject"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:method id="getParentProject" name="GET"><ns2:response><ns2:representation element="project-ref" mediaType="application/xml"/><ns2:representation element="project-ref" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="setParentProject" name="PUT"><ns2:request><ns2:representation element="project-ref" mediaType="application/xml"/><ns2:representation element="project-ref" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="project-ref" mediaType="application/xml"/><ns2:representation element="project-ref" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/buildTypes"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:method id="createBuildType" name="POST"><ns2:doc>Creates a new build configuration by copying existing one.</ns2:doc><ns2:request><ns2:representation element="newBuildTypeDescription" mediaType="application/xml"/><ns2:representation element="newBuildTypeDescription" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="buildType" mediaType="application/xml"/><ns2:representation element="buildType" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="createEmptyBuildType" name="POST"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation element="buildType" mediaType="application/xml"/><ns2:representation element="buildType" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="serveBuildTypesInProject" name="GET"><ns2:response><ns2:representation element="buildTypes" mediaType="application/xml"/><ns2:representation element="buildTypes" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/buildTypes/{btLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildType" name="GET"><ns2:response><ns2:representation element="buildType" mediaType="application/xml"/><ns2:representation element="buildType" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:method id="setProjectFiled" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="serveProjectField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/templates/{btLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeTemplates" name="GET"><ns2:response><ns2:representation element="buildType" mediaType="application/xml"/><ns2:representation element="buildType" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/parameters"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:method id="serveParameters" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="changeAllParameters" name="PUT"><ns2:request><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteAllParameters" name="DELETE"/></ns2:resource><ns2:resource path="/{projectLocator}/buildTypes/{btLocator}/builds/{buildLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildFieldWithProject" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/buildTypes/{btLocator}/builds/{buildLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildWithProject" name="GET"><ns2:response><ns2:representation element="build" mediaType="application/xml"/><ns2:representation element="build" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/newProjectDescription"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:method id="getExampleNewProjectDescription" name="GET"><ns2:doc>Experimental support only.
+ Use this to get an example of the bean to be posted to the /projects request to create a new project</ns2:doc><ns2:response><ns2:representation element="newProjectDescription" mediaType="application/xml"/><ns2:representation element="newProjectDescription" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/buildTypes/{btLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeFieldWithProject" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/buildTypes/{btLocator}/builds"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuilds" name="GET"><ns2:doc>Serves builds matching supplied condition.</ns2:doc><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="status" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="triggeredByUser" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includePersonal" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeCanceled" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="onlyPinned" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="tag" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="agentName" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="sinceBuild" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="sinceDate" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="start" style="query" type="xs:long"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="count" style="query" type="xs:int"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="builds" mediaType="application/xml"/><ns2:representation element="builds" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/vcs-root-instances"><ns2:method id="serveInstances" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="vcs-root-instances" mediaType="application/xml"/><ns2:representation element="vcs-root-instances" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{vcsRootInstanceLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootInstanceLocator" style="template" type="xs:string"/><ns2:method id="serveInstance" name="GET"><ns2:response><ns2:representation element="vcs-root-instance" mediaType="application/xml"/><ns2:representation element="vcs-root-instance" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{vcsRootInstanceLocator}/properties"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootInstanceLocator" style="template" type="xs:string"/><ns2:method id="serveRootInstanceProperties" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{vcsRootInstanceLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootInstanceLocator" style="template" type="xs:string"/><ns2:method id="serveInstanceField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="setInstanceField" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/users"><ns2:method id="createUser" name="POST"><ns2:request><ns2:representation element="user" mediaType="application/xml"/><ns2:representation element="user" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="user" mediaType="*/*"/></ns2:response></ns2:method><ns2:method id="serveUsers" name="GET"><ns2:response><ns2:representation element="users" mediaType="application/xml"/><ns2:representation element="users" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{userLocator}/roles"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="userLocator" style="template" type="xs:string"/><ns2:method id="addRole" name="POST"><ns2:request><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceRoles" name="PUT"><ns2:doc>Replaces user's roles with the submitted ones</ns2:doc><ns2:request><ns2:representation element="roles" mediaType="application/xml"/><ns2:representation element="roles" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="roles" mediaType="application/xml"/><ns2:representation element="roles" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="listRoles" name="GET"><ns2:response><ns2:representation element="roles" mediaType="application/xml"/><ns2:representation element="roles" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{userLocator}/roles/{roleId}/{scope}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="userLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="scope" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="roleId" style="template" type="xs:string"/><ns2:method id="addRoleSimple" name="PUT"><ns2:response><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteRole" name="DELETE"/><ns2:method id="listRole" name="GET"><ns2:response><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addRoleSimplePost" name="POST"/></ns2:resource><ns2:resource path="/{userLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="userLocator" style="template" type="xs:string"/><ns2:method id="serveUser" name="GET"><ns2:response><ns2:representation element="user" mediaType="application/xml"/><ns2:representation element="user" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="updateUser" name="PUT"><ns2:request><ns2:representation element="user" mediaType="application/xml"/><ns2:representation element="user" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="user" mediaType="application/xml"/><ns2:representation element="user" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{userLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="userLocator" style="template" type="xs:string"/><ns2:method id="setUserField" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="serveUserField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{userLocator}/properties"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="userLocator" style="template" type="xs:string"/><ns2:method id="serveUserProperties" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{userLocator}/properties/{name}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="userLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/><ns2:method id="serveUserProperties" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="putUserProperty" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="removeUserProperty" name="DELETE"/></ns2:resource></ns2:resource><ns2:resource path="/app/rest/changes"><ns2:method id="serveChanges" name="GET"><ns2:doc>Lists changes by the specified locator</ns2:doc><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="project" style="query" type="xs:string"><ns2:doc>Change locator</ns2:doc></ns2:param><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildType" style="query" type="xs:string"><ns2:doc>Deprecated, use &quot;locator&quot; parameter instead</ns2:doc></ns2:param><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="build" style="query" type="xs:string"><ns2:doc>Deprecated, use &quot;locator&quot; parameter instead</ns2:doc></ns2:param><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRoot" style="query" type="xs:string"><ns2:doc>Deprecated, use &quot;locator&quot; parameter instead</ns2:doc></ns2:param><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="sinceChange" style="query" type="xs:string"><ns2:doc>Deprecated, use &quot;locator&quot; parameter instead. Note that corresponding locator dimension is &quot;vcsRootInstance&quot;</ns2:doc></ns2:param><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="start" style="query" type="xs:long"><ns2:doc>Deprecated, use &quot;locator&quot; parameter instead</ns2:doc></ns2:param><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="count" style="query" type="xs:int"><ns2:doc>Deprecated, use &quot;locator&quot; parameter instead</ns2:doc></ns2:param><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"><ns2:doc>Deprecated, use &quot;locator&quot; parameter instead</ns2:doc></ns2:param></ns2:request><ns2:response><ns2:representation element="changes" mediaType="application/xml"/><ns2:representation element="changes" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{changeLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getChangeField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/parent-changes"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getParentChanges" name="GET"><ns2:response><ns2:representation element="changes" mediaType="application/xml"/><ns2:representation element="changes" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/parent-revisions"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getChangeParentRevisions" name="GET"><ns2:doc>Experimental support only!</ns2:doc><ns2:response><ns2:representation element="items" mediaType="application/xml"/><ns2:representation element="items" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/vcs-root"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getChangeVCSRoot" name="GET"><ns2:doc>Experimental support only!</ns2:doc><ns2:response><ns2:representation element="vcs-root-instance-ref" mediaType="application/xml"/><ns2:representation element="vcs-root-instance-ref" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/attributes"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getChangeAttributes" name="GET"><ns2:doc>Experimental support only!</ns2:doc><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/duplicates"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getChangeDuplicates" name="GET"><ns2:doc>Experimental support only!</ns2:doc><ns2:response><ns2:representation element="changes" mediaType="application/xml"/><ns2:representation element="changes" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/issues"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getChangeIssue" name="GET"><ns2:doc>Experimental support only!</ns2:doc><ns2:response><ns2:representation element="issues" mediaType="application/xml"/><ns2:representation element="issues" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/buildTypes"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getRelatedBuildTypes" name="GET"><ns2:doc>Experimental support only!</ns2:doc><ns2:response><ns2:representation element="buildTypes" mediaType="application/xml"/><ns2:representation element="buildTypes" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}/firstBuilds"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="getChangeFirstBuilds" name="GET"><ns2:doc>Experimental support only!</ns2:doc><ns2:response><ns2:representation element="builds" mediaType="application/xml"/><ns2:representation element="builds" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{changeLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="changeLocator" style="template" type="xs:string"/><ns2:method id="serveChange" name="GET"><ns2:response><ns2:representation element="change" mediaType="application/xml"/><ns2:representation element="change" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/server"><ns2:method id="serveServerInfo" name="GET"><ns2:response><ns2:representation element="server" mediaType="application/xml"/><ns2:representation element="server" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/backup"><ns2:method id="startBackup" name="POST"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="fileName" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="addTimestamp" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeConfigs" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeDatabase" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeBuildLogs" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includePersonalChanges" style="query" type="xs:boolean"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="getBackupStatus" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/plugins"><ns2:method id="servePlugins" name="GET"><ns2:response><ns2:representation element="plugins" mediaType="application/xml"/><ns2:representation element="plugins" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:method id="serveServerVersion" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/buildTypes"><ns2:method id="serveBuildTypesXML" name="GET"><ns2:response><ns2:representation element="buildTypes" mediaType="application/xml"/><ns2:representation element="buildTypes" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{btLocator}/features/{featureId}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="featureId" style="template" type="xs:string"/><ns2:method id="getFeature" name="GET"><ns2:response><ns2:representation element="feature" mediaType="application/xml"/><ns2:representation element="feature" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteFeature" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/artifact-dependencies/{artifactDepLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="artifactDepLocator" style="template" type="xs:string"/><ns2:method id="getArtifactDep" name="GET"><ns2:response><ns2:representation element="artifact-dependency" mediaType="application/xml"/><ns2:representation element="artifact-dependency" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteArtifactDep" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/agent-requirements/{agentRequirementLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="agentRequirementLocator" style="template" type="xs:string"/><ns2:method id="getAgentRequirement" name="GET"><ns2:response><ns2:representation element="agent-requirement" mediaType="application/xml"/><ns2:representation element="agent-requirement" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteAgentRequirement" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/steps"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="addStep" name="POST"><ns2:request><ns2:representation element="step" mediaType="application/xml"/><ns2:representation element="step" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="step" mediaType="application/xml"/><ns2:representation element="step" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceSteps" name="PUT"><ns2:request><ns2:representation element="steps" mediaType="application/xml"/><ns2:representation element="steps" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="steps" mediaType="application/xml"/><ns2:representation element="steps" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="getSteps" name="GET"><ns2:response><ns2:representation element="steps" mediaType="application/xml"/><ns2:representation element="steps" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/features"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getFeatures" name="GET"><ns2:response><ns2:representation element="features" mediaType="application/xml"/><ns2:representation element="features" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addFeature" name="POST"><ns2:request><ns2:representation element="feature" mediaType="*/*"/></ns2:request><ns2:response><ns2:representation element="feature" mediaType="application/xml"/><ns2:representation element="feature" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceFeatures" name="PUT"><ns2:request><ns2:representation element="features" mediaType="application/xml"/><ns2:representation element="features" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="features" mediaType="application/xml"/><ns2:representation element="features" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/investigations"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getInvestigations" name="GET"><ns2:response><ns2:representation element="investigations" mediaType="application/xml"/><ns2:representation element="investigations" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/vcs-root-entries"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getVcsRootEntries" name="GET"><ns2:response><ns2:representation element="vcs-root-entries" mediaType="application/xml"/><ns2:representation element="vcs-root-entries" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceVcsRootEntries" name="PUT"><ns2:request><ns2:representation element="vcs-root-entries" mediaType="application/xml"/><ns2:representation element="vcs-root-entries" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="vcs-root-entries" mediaType="application/xml"/><ns2:representation element="vcs-root-entries" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addVcsRootEntry" name="POST"><ns2:request><ns2:representation element="vcs-root-entry" mediaType="application/xml"/><ns2:representation element="vcs-root-entry" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="vcs-root-entry" mediaType="application/xml"/><ns2:representation element="vcs-root-entry" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/vcs-root-entries/{id}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="id" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getVcsRootEntry" name="GET"><ns2:response><ns2:representation element="vcs-root-entry" mediaType="application/xml"/><ns2:representation element="vcs-root-entry" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteVcsRootEntry" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/snapshot-dependencies"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="replaceSnapshotDeps" name="PUT"><ns2:doc>Replaces snapshot dependency with those sent in request.</ns2:doc><ns2:request><ns2:representation element="snapshot-dependencies" mediaType="application/xml"/><ns2:representation element="snapshot-dependencies" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="snapshot-dependencies" mediaType="application/xml"/><ns2:representation element="snapshot-dependencies" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addSnapshotDep" name="POST"><ns2:doc>Creates new snapshot dependency. 'id' attribute is ignored in the submitted descriptor.
+ Reports error if new dependency cannot be created (e.g. another dependency on the specified build configuration already exists).</ns2:doc><ns2:request><ns2:representation element="snapshot-dependency" mediaType="application/xml"/><ns2:representation element="snapshot-dependency" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="snapshot-dependency" mediaType="application/xml"/><ns2:representation element="snapshot-dependency" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="getSnapshotDeps" name="GET"><ns2:response><ns2:representation element="snapshot-dependencies" mediaType="application/xml"/><ns2:representation element="snapshot-dependencies" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/snapshot-dependencies/{snapshotDepLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="snapshotDepLocator" style="template" type="xs:string"/><ns2:method id="deleteSnapshotDep" name="DELETE"/><ns2:method id="getSnapshotDep" name="GET"><ns2:response><ns2:representation element="snapshot-dependency" mediaType="application/xml"/><ns2:representation element="snapshot-dependency" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/triggers"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="replaceTriggers" name="PUT"><ns2:doc>Replaces trigger with those sent inthe request.</ns2:doc><ns2:request><ns2:representation element="triggers" mediaType="application/xml"/><ns2:representation element="triggers" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="triggers" mediaType="application/xml"/><ns2:representation element="triggers" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addTrigger" name="POST"><ns2:doc>Creates new trigger. 'id' attribute is ignored in the submitted descriptor.
+ Reports error if new trigger cannot be created (e.g. only single trigger of the type is allowed for a build configuration).</ns2:doc><ns2:request><ns2:representation element="trigger" mediaType="application/xml"/><ns2:representation element="trigger" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="trigger" mediaType="application/xml"/><ns2:representation element="trigger" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="getTriggers" name="GET"><ns2:response><ns2:representation element="triggers" mediaType="application/xml"/><ns2:representation element="triggers" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/triggers/{triggerLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="triggerLocator" style="template" type="xs:string"/><ns2:method id="deleteTrigger" name="DELETE"/><ns2:method id="getTrigger" name="GET"><ns2:response><ns2:representation element="trigger" mediaType="application/xml"/><ns2:representation element="trigger" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/triggers/{triggerLocator}/{fieldName}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="triggerLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="fieldName" style="template" type="xs:string"/><ns2:method id="getTriggerSetting" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="changeTriggerSetting" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/agent-requirements"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="replaceAgentRequirements" name="PUT"><ns2:doc>Replaces agent requirements with those sent in the request.</ns2:doc><ns2:request><ns2:representation element="agent-requirements" mediaType="application/xml"/><ns2:representation element="agent-requirements" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="agent-requirements" mediaType="application/xml"/><ns2:representation element="agent-requirements" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addAgentRequirement" name="POST"><ns2:doc>Creates new agent requirement. 'id' attribute is ignored in the submitted descriptor.
+ Reports error if new requirement cannot be created (e.g. another requirement is present for the parameter).</ns2:doc><ns2:request><ns2:representation element="agent-requirement" mediaType="application/xml"/><ns2:representation element="agent-requirement" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="agent-requirement" mediaType="application/xml"/><ns2:representation element="agent-requirement" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="getAgentRequirements" name="GET"><ns2:response><ns2:representation element="agent-requirements" mediaType="application/xml"/><ns2:representation element="agent-requirements" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/vcs-root-instances"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getCurrentVcsInstances" name="GET"><ns2:response><ns2:representation element="vcs-root-instances" mediaType="application/xml"/><ns2:representation element="vcs-root-instances" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/builds/{buildLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildWithProject" name="GET"><ns2:response><ns2:representation element="build" mediaType="application/xml"/><ns2:representation element="build" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/builds/{buildLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/branches"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBranches" name="GET"><ns2:response><ns2:representation element="branches" mediaType="application/xml"/><ns2:representation element="branches" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/vcs-labeling"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getVCSLabelingOptions" name="GET"><ns2:response><ns2:representation element="vcs-labeling" mediaType="application/xml"/><ns2:representation element="vcs-labeling" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="setVCSLabelingOptions" name="PUT"><ns2:request><ns2:representation element="vcs-labeling" mediaType="application/xml"/><ns2:representation element="vcs-labeling" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="vcs-labeling" mediaType="application/xml"/><ns2:representation element="vcs-labeling" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/newBuildTypeDescription"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getExampleNewProjectDescription" name="GET"><ns2:doc>Experimental support only.
+ Use this to get an example of the bean to be posted to the /buildTypes request to create a new build type</ns2:doc><ns2:response><ns2:representation element="newBuildTypeDescription" mediaType="application/xml"/><ns2:representation element="newBuildTypeDescription" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/builds"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuilds" name="GET"><ns2:doc>Serves builds matching supplied condition.</ns2:doc><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="status" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="triggeredByUser" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includePersonal" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeCanceled" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="onlyPinned" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="tag" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="agentName" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="sinceBuild" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="sinceDate" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="start" style="query" type="xs:long"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="count" style="query" type="xs:int"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="builds" mediaType="application/xml"/><ns2:representation element="builds" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/steps/{stepId}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="stepId" style="template" type="xs:string"/><ns2:method id="deleteStep" name="DELETE"/><ns2:method id="getStep" name="GET"><ns2:response><ns2:representation element="step" mediaType="application/xml"/><ns2:representation element="step" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeXML" name="GET"><ns2:doc>Serves build configuration or templates according to the locator.</ns2:doc><ns2:response><ns2:representation element="buildType" mediaType="application/xml"/><ns2:representation element="buildType" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteBuildType" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="setBuildTypeField" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/buildTags"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeBuildsTags" name="GET"><ns2:response><ns2:representation element="tags" mediaType="application/xml"/><ns2:representation element="tags" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/parameters"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeParameters" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="changeBuildTypeParameters" name="PUT"><ns2:request><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteAllBuildTypeParameters" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/parameters/{name}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/><ns2:method id="serveBuildTypeParameter" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="putBuildTypeParameter" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="deleteBuildTypeParameter" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/settings/{name}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/><ns2:method id="serveBuildTypeSettings" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="putBuildTypeSetting" name="PUT"><ns2:request><ns2:representation mediaType="*/*"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/settings"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeSettings" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceBuildTypeSettings" name="PUT"><ns2:request><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/template"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="serveBuildTypeTemplate" name="GET"><ns2:response><ns2:representation element="buildType-ref" mediaType="application/xml"/><ns2:representation element="buildType-ref" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="getTemplateAssociation" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation element="buildType-ref" mediaType="application/xml"/><ns2:representation element="buildType-ref" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteTemplateAssociation" name="DELETE"/></ns2:resource><ns2:resource path="/{btLocator}/steps/{stepId}/parameters"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="stepId" style="template" type="xs:string"/><ns2:method id="getStepParameters" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceStepParameters" name="PUT"><ns2:request><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/steps/{stepId}/parameters/{parameterName}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="stepId" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="parameterName" style="template" type="xs:string"/><ns2:method id="getStepParameter" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="addStepParameter" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/steps/{stepId}/{fieldName}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="fieldName" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="stepId" style="template" type="xs:string"/><ns2:method id="getStepSetting" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="changeStepSetting" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/features/{featureId}/parameters"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="featureId" style="template" type="xs:string"/><ns2:method id="getFeatureParameters" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceFeatureParameters" name="PUT"><ns2:request><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/features/{featureId}/parameters/{parameterName}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="featureId" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="parameterName" style="template" type="xs:string"/><ns2:method id="getFeatureParameter" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="addFeatureParameter" name="PUT"><ns2:request><ns2:representation mediaType="*/*"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/features/{featureId}/{name}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="featureId" style="template" type="xs:string"/><ns2:method id="getFeatureSetting" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="changeFeatureSetting" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{btLocator}/artifact-dependencies"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:method id="getArtifactDeps" name="GET"><ns2:response><ns2:representation element="artifact-dependencies" mediaType="application/xml"/><ns2:representation element="artifact-dependencies" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="replaceArtifactDeps" name="PUT"><ns2:doc>Replaces the dependencies to those sent in the request.</ns2:doc><ns2:request><ns2:representation element="artifact-dependencies" mediaType="application/xml"/><ns2:representation element="artifact-dependencies" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="artifact-dependencies" mediaType="application/xml"/><ns2:representation element="artifact-dependencies" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addArtifactDep" name="POST"><ns2:request><ns2:representation element="artifact-dependency" mediaType="*/*"/></ns2:request><ns2:response><ns2:representation element="artifact-dependency" mediaType="application/xml"/><ns2:representation element="artifact-dependency" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/userGroups"><ns2:method id="addGroup" name="POST"><ns2:request><ns2:representation element="group" mediaType="application/xml"/><ns2:representation element="group" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="group" mediaType="application/xml"/><ns2:representation element="group" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="serveGroups" name="GET"><ns2:response><ns2:representation element="groups" mediaType="application/xml"/><ns2:representation element="groups" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{groupLocator}/roles"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="groupLocator" style="template" type="xs:string"/><ns2:method id="addRole" name="POST"><ns2:request><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="listRoles" name="GET"><ns2:response><ns2:representation element="roles" mediaType="application/xml"/><ns2:representation element="roles" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addRolePut" name="PUT"><ns2:request><ns2:representation element="roles" mediaType="application/xml"/><ns2:representation element="roles" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="roles" mediaType="application/xml"/><ns2:representation element="roles" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{groupLocator}/roles/{roleId}/{scope}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="groupLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="scope" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="roleId" style="template" type="xs:string"/><ns2:method id="addRoleSimple" name="POST"><ns2:response><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteRole" name="DELETE"/><ns2:method id="listRole" name="GET"><ns2:response><ns2:representation element="role" mediaType="application/xml"/><ns2:representation element="role" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{groupLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="groupLocator" style="template" type="xs:string"/><ns2:method id="deleteGroup" name="DELETE"/><ns2:method id="serveGroup" name="GET"><ns2:response><ns2:representation element="group" mediaType="application/xml"/><ns2:representation element="group" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/debug"><ns2:doc>Provides some debug abilities for the server. Experimental only. Should be used with caution or better not used if not advised by JetBrains
+ These should never be used for non-debug purposes and the API here can change in future versions of TeamCity without any notice.</ns2:doc><ns2:resource path="/database/query/{query}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="query" style="template" type="xs:string"/><ns2:method id="executeDBQuery" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="fieldDelimiter" style="query" type="xs:string" default=", "/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="count" style="query" type="xs:int" default="1000"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain; charset=UTF-8"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/vcsCheckingForChangesQueue"><ns2:method id="scheduleCheckingForChanges" name="POST"><ns2:doc>Experimental use only!</ns2:doc><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="vcs-root-instances" mediaType="application/xml"/><ns2:representation element="vcs-root-instances" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/database/tables"><ns2:method id="listDBTables" name="GET"><ns2:response><ns2:representation mediaType="text/plain; charset=UTF-8"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/builds"><ns2:method id="serveAllBuilds" name="GET"><ns2:doc>Serves builds matching supplied condition.</ns2:doc><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildType" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="status" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="triggeredByUser" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includePersonal" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeCanceled" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="onlyPinned" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="tag" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="agentName" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="sinceBuild" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="sinceDate" style="query" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="start" style="query" type="xs:long"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="count" style="query" type="xs:int"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="builds" mediaType="application/xml"/><ns2:representation element="builds" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{buildLocator}/resulting-properties/{propertyName}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="propertyName" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="getParameter" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/artifacts"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="getArtifacts" name="GET"><ns2:doc>More user-friendly URL for &quot;/{buildLocator}/artifacts/children&quot; one.</ns2:doc><ns2:response><ns2:representation element="files" mediaType="application/xml"/><ns2:representation element="files" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/artifacts/metadata{path:(/.*)?}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="path" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="getArtifactMetadata" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="resolveParameters" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="file" mediaType="application/xml"/><ns2:representation element="file" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="deleteBuild" name="DELETE"/><ns2:method id="serveBuild" name="GET"><ns2:response><ns2:representation element="build" mediaType="application/xml"/><ns2:representation element="build" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/pin/"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="pinBuild" name="PUT"><ns2:doc>Pins a build</ns2:doc><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request></ns2:method><ns2:method id="getPinned" name="GET"><ns2:doc>Fetches current build pinned status.</ns2:doc><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="unpinBuild" name="DELETE"><ns2:doc>Unpins a build</ns2:doc><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/tags/"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveTags" name="GET"><ns2:response><ns2:representation element="tags" mediaType="application/xml"/><ns2:representation element="tags" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="addTags" name="POST"><ns2:doc>Adds a set of tags to a build</ns2:doc><ns2:request><ns2:representation element="tags" mediaType="application/xml"/><ns2:representation element="tags" mediaType="application/json"/></ns2:request></ns2:method><ns2:method id="addTag" name="POST"><ns2:doc>Adds a single tag to a build</ns2:doc><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="replaceTags" name="PUT"><ns2:doc>Replaces build's tags.</ns2:doc><ns2:request><ns2:representation element="tags" mediaType="application/xml"/><ns2:representation element="tags" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="tags" mediaType="application/xml"/><ns2:representation element="tags" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/resulting-properties/"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildActualParameters" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/artifacts/children{path:(/.*)?}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="path" style="template" type="xs:string" default=""/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="getArtifactChildren" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="resolveParameters" style="query" type="xs:boolean"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="files" mediaType="application/xml"/><ns2:representation element="files" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/artifacts/content{path:(/.*)?}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="path" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="getArtifactContent" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="resolveParameters" style="query" type="xs:boolean"/></ns2:request><ns2:response><ns2:representation mediaType="*/*"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/artifacts/files{path:(/.*)?}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="path" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="getArtifactFilesContent" name="GET"><ns2:response><ns2:representation mediaType="*/*"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/sources/files/{fileName:.+}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="fileName" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveSourceFile" name="GET"><ns2:response><ns2:representation mediaType="application/octet-stream"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/related-issues"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildRelatedIssuesOld" name="GET"><ns2:response><ns2:representation element="issuesUsages" mediaType="application/xml"/><ns2:representation element="issuesUsages" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/relatedIssues"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildRelatedIssues" name="GET"><ns2:response><ns2:representation element="issuesUsages" mediaType="application/xml"/><ns2:representation element="issuesUsages" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildFieldByBuildOnly" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/statistics/"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildStatisticValues" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/statistics/{name}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildStatisticValue" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{buildLocator}/comment"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="replaceComment" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request></ns2:method><ns2:method id="deleteComment" name="DELETE"/></ns2:resource><ns2:resource path="/{buildLocator}/statusIcon"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildStatusIcon" name="GET"><ns2:response><ns2:representation mediaType="*/*"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/cctray"><ns2:resource path="/projects.xml"><ns2:method id="serveProjects" name="GET"><ns2:response><ns2:representation element="Projects" mediaType="application/xml"/><ns2:representation element="Projects" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/vcs-roots"><ns2:method id="addRoot" name="POST"><ns2:request><ns2:representation element="vcs-root" mediaType="application/xml"/><ns2:representation element="vcs-root" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="vcs-root" mediaType="application/xml"/><ns2:representation element="vcs-root" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="serveRoots" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="locator" style="query" type="xs:string"/></ns2:request><ns2:response><ns2:representation element="vcs-roots" mediaType="application/xml"/><ns2:representation element="vcs-roots" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{vcsRootLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"/><ns2:method id="setField" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="serveField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{vcsRootLocator}/properties/{name}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/><ns2:method id="putParameter" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="serveProperty" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="deleteParameter" name="DELETE"/></ns2:resource><ns2:resource path="/{vcsRootLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"/><ns2:method id="serveRoot" name="GET"><ns2:response><ns2:representation element="vcs-root" mediaType="application/xml"/><ns2:representation element="vcs-root" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteRoot" name="DELETE"/></ns2:resource><ns2:resource path="/{vcsRootLocator}/instances/{vcsRootInstanceLocator}/properties"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootInstanceLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"/><ns2:method id="serveRootInstanceProperties" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{vcsRootLocator}/instances/{vcsRootInstanceLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootInstanceLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"/><ns2:method id="serveInstanceField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="setInstanceField" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{vcsRootLocator}/instances"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"/><ns2:method id="serveRootInstances" name="GET"><ns2:response><ns2:representation element="vcs-root-instances" mediaType="application/xml"/><ns2:representation element="vcs-root-instances" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{vcsRootLocator}/instances/{vcsRootInstanceLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootInstanceLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"><ns2:doc>this is effectively ignored as vcsRootInstanceLocator should specify instance fully</ns2:doc></ns2:param><ns2:method id="serveRootInstance" name="GET"><ns2:response><ns2:representation element="vcs-root-instance" mediaType="application/xml"/><ns2:representation element="vcs-root-instance" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{vcsRootLocator}/properties"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="vcsRootLocator" style="template" type="xs:string"/><ns2:method id="serveProperties" name="GET"><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="changProperties" name="PUT"><ns2:request><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:request><ns2:response><ns2:representation element="properties" mediaType="application/xml"/><ns2:representation element="properties" mediaType="application/json"/></ns2:response></ns2:method><ns2:method id="deleteAllProperties" name="DELETE"/></ns2:resource></ns2:resource><ns2:resource path="/app/rest"><ns2:method id="serveRoot" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:resource path="/version"><ns2:method id="serveApiVersion" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/info"><ns2:method id="servePluginInfo" name="GET"><ns2:response><ns2:representation element="plugin" mediaType="application/xml"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{projectLocator}/{btLocator}/{buildLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="projectLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="btLocator" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="buildLocator" style="template" type="xs:string"/><ns2:method id="serveBuildFieldShort" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource></ns2:resource><ns2:resource path="/app/rest/agents"><ns2:method id="serveAgents" name="GET"><ns2:request><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeDisconnected" style="query" type="xs:boolean" default="true"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="includeUnauthorized" style="query" type="xs:boolean" default="true"/></ns2:request><ns2:response><ns2:representation element="agents-ref" mediaType="application/xml"/><ns2:representation element="agents-ref" mediaType="application/json"/></ns2:response></ns2:method><ns2:resource path="/{agentLocator}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="agentLocator" style="template" type="xs:string"/><ns2:method id="serveAgent" name="GET"><ns2:response><ns2:representation element="agent" mediaType="application/xml"/><ns2:representation element="agent" mediaType="application/json"/></ns2:response></ns2:method></ns2:resource><ns2:resource path="/{agentLocator}/{field}"><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="field" style="template" type="xs:string"/><ns2:param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="agentLocator" style="template" type="xs:string"/><ns2:method id="serveAgentField" name="GET"><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method><ns2:method id="setAgentField" name="PUT"><ns2:request><ns2:representation mediaType="text/plain"/></ns2:request><ns2:response><ns2:representation mediaType="text/plain"/></ns2:response></ns2:method></ns2:resource></ns2:resource></ns2:resources></ns2:application>
\ No newline at end of file
diff --git a/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/storage.version b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/storage.version
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/parentFiles/parentDir/storage.version
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git a/ttorrent-master/tests/src/test/resources/torrents/file1.jar.torrent b/ttorrent-master/tests/src/test/resources/torrents/file1.jar.torrent
new file mode 100644
index 0000000..e169744
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/torrents/file1.jar.torrent
Binary files differ
diff --git a/ttorrent-master/tests/src/test/resources/torrents/file2.jar.torrent b/ttorrent-master/tests/src/test/resources/torrents/file2.jar.torrent
new file mode 100644
index 0000000..85d3a3a
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/torrents/file2.jar.torrent
Binary files differ
diff --git a/ttorrent-master/tests/src/test/resources/torrents/parentDir.linux.torrent b/ttorrent-master/tests/src/test/resources/torrents/parentDir.linux.torrent
new file mode 100644
index 0000000..fa4ce56
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/torrents/parentDir.linux.torrent
@@ -0,0 +1 @@
+d8:announce30:http://localhost:6969/announce10:created by5:Test213:creation datei1376051000e4:infod5:filesld6:lengthi252788e4:pathl17:AccuRevCommon.jareed6:lengthi188910e4:pathl23:commons-io-cio2.5_3.jareed6:lengthi240e4:pathl28:commons-io-cio2.5_3.jar.linkeed6:lengthi82297e4:pathl5:inDir16:application.wadleed6:lengthi1e4:pathl15:storage.versioneee4:name9:parentDir12:piece lengthi524288e6:pieces20:žNÜJ¾MØ=û¡Ñ]‹vb¦£6ee
\ No newline at end of file
diff --git a/ttorrent-master/tests/src/test/resources/torrents/parentDir.win.torrent b/ttorrent-master/tests/src/test/resources/torrents/parentDir.win.torrent
new file mode 100644
index 0000000..210292d
--- /dev/null
+++ b/ttorrent-master/tests/src/test/resources/torrents/parentDir.win.torrent
@@ -0,0 +1 @@
+d8:announce30:http://localhost:6969/announce10:created by5:Test213:creation datei1376051000e4:infod5:filesld6:lengthi252788e4:pathl17:AccuRevCommon.jareed6:lengthi188910e4:pathl23:commons-io-cio2.5_3.jareed6:lengthi241e4:pathl28:commons-io-cio2.5_3.jar.linkeed6:lengthi82305e4:pathl5:inDir16:application.wadleed6:lengthi1e4:pathl15:storage.versioneee4:name9:parentDir12:piece lengthi524288e6:pieces20:Tvhdé^ùÍv—œ¤}ïÚKF±ee
\ No newline at end of file
diff --git a/ttorrent-master/ttorrent-client/pom.xml b/ttorrent-master/ttorrent-client/pom.xml
new file mode 100644
index 0000000..3b55515
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/pom.xml
@@ -0,0 +1,46 @@
+<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/client</name>
+    <url>http://turn.github.com/ttorrent/</url>
+    <artifactId>ttorrent-client</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>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-network</artifactId>
+            <version>1.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-common</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-test-api</artifactId>
+            <version>1.0</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/AnnounceableInformationImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/AnnounceableInformationImpl.java
new file mode 100644
index 0000000..fdf0f60
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/AnnounceableInformationImpl.java
@@ -0,0 +1,70 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.TorrentHash;
+
+import java.util.List;
+
+class AnnounceableInformationImpl implements AnnounceableInformation {
+
+  private final long uploaded;
+  private final long downloaded;
+  private final long left;
+  private final TorrentHash torrentHash;
+  private final List<List<String>> announceUrls;
+  private final String announce;
+
+  public AnnounceableInformationImpl(long uploaded,
+                                     long downloaded,
+                                     long left,
+                                     TorrentHash torrentHash,
+                                     List<List<String>> announceUrls,
+                                     String announce) {
+    this.uploaded = uploaded;
+    this.downloaded = downloaded;
+    this.left = left;
+    this.torrentHash = torrentHash;
+    this.announceUrls = announceUrls;
+    this.announce = announce;
+  }
+
+  @Override
+  public long getUploaded() {
+    return uploaded;
+  }
+
+  @Override
+  public long getDownloaded() {
+    return downloaded;
+  }
+
+  @Override
+  public long getLeft() {
+    return left;
+  }
+
+  @Override
+  public List<List<String>> getAnnounceList() {
+    return announceUrls;
+  }
+
+  @Override
+  public String getAnnounce() {
+    return announce;
+  }
+
+  @Override
+  public byte[] getInfoHash() {
+    return torrentHash.getInfoHash();
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return torrentHash.getHexInfoHash();
+  }
+
+  @Override
+  public String toString() {
+    return "announceable torrent " + torrentHash.getHexInfoHash() + " for trackers " + announceUrls;
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientState.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientState.java
new file mode 100644
index 0000000..526106e
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/ClientState.java
@@ -0,0 +1,10 @@
+package com.turn.ttorrent.client;
+
+public enum ClientState {
+  WAITING,
+  VALIDATING,
+  SHARING,
+  SEEDING,
+  ERROR,
+  DONE
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/CommunicationManager.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/CommunicationManager.java
new file mode 100644
index 0000000..4de7ed3
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/CommunicationManager.java
@@ -0,0 +1,865 @@
+/**
+ * Copyright (C) 2011-2012 Turn, Inc.
+ * <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.client;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.client.announce.*;
+import com.turn.ttorrent.client.network.CountLimitConnectionAllower;
+import com.turn.ttorrent.client.network.OutgoingConnectionListener;
+import com.turn.ttorrent.client.network.StateChannelListener;
+import com.turn.ttorrent.client.peer.PeerActivityListener;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.client.storage.FairPieceStorageFactory;
+import com.turn.ttorrent.client.storage.FileCollectionStorage;
+import com.turn.ttorrent.client.storage.PieceStorage;
+import com.turn.ttorrent.client.storage.PieceStorageFactory;
+import com.turn.ttorrent.common.*;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.PeerMessage;
+import com.turn.ttorrent.network.*;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.turn.ttorrent.Constants.DEFAULT_SOCKET_CONNECTION_TIMEOUT_MILLIS;
+import static com.turn.ttorrent.common.protocol.AnnounceRequestMessage.RequestEvent.*;
+
+/**
+ * A pure-java BitTorrent client.
+ * <p/>
+ * <p>
+ * A BitTorrent client in its bare essence shares a given torrent. If the
+ * torrent is not complete locally, it will continue to download it. If or
+ * after the torrent is complete, the client may eventually continue to seed it
+ * for other clients.
+ * </p>
+ * <p/>
+ * <p>
+ * This BitTorrent client implementation is made to be simple to embed and
+ * simple to use. First, initialize a ShareTorrent object from a torrent
+ * meta-info source (either a file or a byte array, see
+ * com.turn.ttorrent.SharedTorrent for how to create a SharedTorrent object).
+ * </p>
+ *
+ * @author mpetazzoni
+ *
+ * 实现从torrent元信息源(文件或字节数组均可)初始化一个 ShareTorrent 对象。
+ */
+public class CommunicationManager implements AnnounceResponseListener, PeerActivityListener, Context, ConnectionManagerContext {
+
+  protected static final Logger logger = TorrentLoggerFactory.getLogger(CommunicationManager.class);
+
+  public static final String BITTORRENT_ID_PREFIX = "-TO0042-";
+
+  private AtomicBoolean stop = new AtomicBoolean(false);
+
+  private Announce announce;
+
+  private volatile boolean myStarted = false;
+  private final TorrentLoader myTorrentLoader;
+  private final TorrentsStorage torrentsStorage;
+  private final CountLimitConnectionAllower myInConnectionAllower;
+  private final CountLimitConnectionAllower myOutConnectionAllower;
+  private final AtomicInteger mySendBufferSize;
+  private final AtomicInteger myReceiveBufferSize;
+  private final PeersStorage peersStorage;
+  private volatile ConnectionManager myConnectionManager;
+  private final ExecutorService myExecutorService;
+  private final ExecutorService myPieceValidatorExecutor;
+
+  /**
+   * @param workingExecutor        executor service for run connection worker and process incoming data. Must have a pool size at least 2
+   *                               处理连接相关 建立来凝结,接收处理数据
+   * @param pieceValidatorExecutor executor service for calculation sha1 hashes of downloaded pieces
+   *                               计算下载片段的哈希值
+   */
+  public CommunicationManager(ExecutorService workingExecutor, ExecutorService pieceValidatorExecutor) {
+    this(workingExecutor, pieceValidatorExecutor, new TrackerClientFactoryImpl());
+  }
+
+  /**
+   * @param workingExecutor        executor service for run connection worker and process incoming data. Must have a pool size at least 2
+   * @param pieceValidatorExecutor executor service for calculation sha1 hashes of downloaded pieces
+   * @param trackerClientFactory   factory which creates instances for communication with tracker
+   */
+  public CommunicationManager(ExecutorService workingExecutor, ExecutorService pieceValidatorExecutor, TrackerClientFactory trackerClientFactory) {
+    this.announce = new Announce(this, trackerClientFactory);// 负责于tracker进行通信的
+    this.torrentsStorage = new TorrentsStorage();// torrent文件存储
+    this.peersStorage = new PeersStorage();// 结点存储
+    this.mySendBufferSize = new AtomicInteger();// 整数 发送字节大小
+    this.myTorrentLoader = new TorrentLoaderImpl(this.torrentsStorage);//将加载的种子转化为分享的种子
+    this.myReceiveBufferSize = new AtomicInteger();// 整数 接收字节大小
+    this.myInConnectionAllower = new CountLimitConnectionAllower(peersStorage);
+    this.myOutConnectionAllower = new CountLimitConnectionAllower(peersStorage);
+    this.myExecutorService = workingExecutor;
+    myPieceValidatorExecutor = pieceValidatorExecutor;
+  }
+
+  /**
+   * Adds torrent to storage, validate downloaded files and start seeding and leeching the torrent
+   *
+   * @param dotTorrentFilePath path to torrent metadata file
+   * @param downloadDirPath    path to directory where downloaded files are placed
+   * @return {@link TorrentManager} instance for monitoring torrent state
+   * @throws IOException if IO error occurs in reading metadata file
+   */
+  //通过用户提供的种子文件路径(dotTorrentFilePath)来添加种子到存储中
+  //并将下载的文件保存到指定的本地目录(downloadDirPath)。
+  public TorrentManager addTorrent(String dotTorrentFilePath, String downloadDirPath) throws IOException {
+    return addTorrent(dotTorrentFilePath, downloadDirPath, FairPieceStorageFactory.INSTANCE);
+  }
+
+  /**
+   * Adds torrent to storage with specified listeners, validate downloaded files and start seeding and leeching the torrent
+   *
+   * @param dotTorrentFilePath path to torrent metadata file
+   * @param downloadDirPath    path to directory where downloaded files are placed
+   * @param listeners          specified listeners
+   * @return {@link TorrentManager} instance for monitoring torrent state
+   * @throws IOException if IO error occurs in reading metadata file
+   */
+  public TorrentManager addTorrent(String dotTorrentFilePath, String downloadDirPath, List<TorrentListener> listeners) throws IOException {
+    return addTorrent(dotTorrentFilePath, downloadDirPath, FairPieceStorageFactory.INSTANCE, listeners);
+  }
+
+  /**
+   * Adds torrent to storage with specified {@link PieceStorageFactory}.
+   * It can be used for skipping initial validation of data
+   *
+   * @param dotTorrentFilePath path to torrent metadata file
+   * @param downloadDirPath    path to directory where downloaded files are placed
+   * @param pieceStorageFactory factory for creating {@link PieceStorage}.
+   * @return {@link TorrentManager} instance for monitoring torrent state
+   * @throws IOException if IO error occurs in reading metadata file
+   */
+  public TorrentManager addTorrent(String dotTorrentFilePath,
+                                   String downloadDirPath,
+                                   PieceStorageFactory pieceStorageFactory) throws IOException {
+    return addTorrent(dotTorrentFilePath, downloadDirPath, pieceStorageFactory, Collections.<TorrentListener>emptyList());
+  }
+
+  /**
+   * Adds torrent to storage with specified {@link PieceStorageFactory}.
+   * It can be used for skipping initial validation of data
+   *
+   * @param dotTorrentFilePath path to torrent metadata file
+   * @param downloadDirPath    path to directory where downloaded files are placed
+   * @param pieceStorageFactory factory for creating {@link PieceStorage}.
+   * @return {@link TorrentManager} instance for monitoring torrent state
+   * @throws IOException if IO error occurs in reading metadata file
+   */
+  public TorrentManager addTorrent(String dotTorrentFilePath,
+                                   String downloadDirPath,
+                                   PieceStorageFactory pieceStorageFactory,
+                                   List<TorrentListener> listeners) throws IOException {
+    FileMetadataProvider metadataProvider = new FileMetadataProvider(dotTorrentFilePath);
+    TorrentMetadata metadata = metadataProvider.getTorrentMetadata();
+    FileCollectionStorage fileCollectionStorage = FileCollectionStorage.create(metadata, new File(downloadDirPath));
+    PieceStorage pieceStorage = pieceStorageFactory.createStorage(metadata, fileCollectionStorage);
+    return addTorrent(metadataProvider, pieceStorage, listeners);
+  }
+
+  /**
+   * Adds torrent to storage with any storage and metadata source
+   *
+   * @param metadataProvider specified metadata source
+   * @param pieceStorage     specified storage of pieces
+   * @return {@link TorrentManager} instance for monitoring torrent state
+   * @throws IOException if IO error occurs in reading metadata file
+   */
+  public TorrentManager addTorrent(TorrentMetadataProvider metadataProvider, PieceStorage pieceStorage) throws IOException {
+    return addTorrent(metadataProvider, pieceStorage, Collections.<TorrentListener>emptyList());
+  }
+
+  /**
+   * Adds torrent to storage with any storage, metadata source and specified listeners
+   *
+   * @param metadataProvider specified metadata source
+   * @param pieceStorage     specified storage of pieces
+   * @param listeners        specified listeners
+   * @return {@link TorrentManager} instance for monitoring torrent state
+   * @throws IOException if IO error occurs in reading metadata file
+   */
+  public TorrentManager addTorrent(TorrentMetadataProvider metadataProvider,
+                                   PieceStorage pieceStorage,
+                                   List<TorrentListener> listeners) throws IOException {
+    TorrentMetadata torrentMetadata = metadataProvider.getTorrentMetadata();
+    EventDispatcher eventDispatcher = new EventDispatcher();
+    for (TorrentListener listener : listeners) {
+      eventDispatcher.addListener(listener);
+    }
+    final LoadedTorrentImpl loadedTorrent = new LoadedTorrentImpl(
+            new TorrentStatistic(),
+            metadataProvider,
+            torrentMetadata,
+            pieceStorage,
+            eventDispatcher);
+
+    if (pieceStorage.isFinished()) {
+      loadedTorrent.getTorrentStatistic().setLeft(0);
+    } else {
+      long left = calculateLeft(pieceStorage, torrentMetadata);
+      loadedTorrent.getTorrentStatistic().setLeft(left);
+    }
+    eventDispatcher.multicaster().validationComplete(pieceStorage.getAvailablePieces().cardinality(), torrentMetadata.getPiecesCount());
+
+    this.torrentsStorage.addTorrent(loadedTorrent.getTorrentHash().getHexInfoHash(), loadedTorrent);
+    forceAnnounceAndLogError(loadedTorrent, pieceStorage.isFinished() ? COMPLETED : STARTED);
+    logger.debug(String.format("Added torrent %s (%s)", loadedTorrent, loadedTorrent.getTorrentHash().getHexInfoHash()));
+    return new TorrentManagerImpl(eventDispatcher, loadedTorrent.getTorrentHash());
+    //addtorrent最终返回的数据
+  }
+
+  // 计算剩余大小,不用管
+  private long calculateLeft(PieceStorage pieceStorage, TorrentMetadata torrentMetadata) {
+
+    long size = 0;
+    for (TorrentFile torrentFile : torrentMetadata.getFiles()) {
+      size += torrentFile.size;
+    }
+
+    int pieceLength = torrentMetadata.getPieceLength();
+    long result = 0;
+    BitSet availablePieces = pieceStorage.getAvailablePieces();
+    for (int i = 0; i < torrentMetadata.getPiecesCount(); i++) {
+      if (availablePieces.get(i)) {
+        continue;
+      }
+      result += Math.min(pieceLength, size - i * pieceLength);
+    }
+    return result;
+  }
+
+  private void forceAnnounceAndLogError(LoadedTorrent torrent, AnnounceRequestMessage.RequestEvent event) {
+    try {
+      this.announce.forceAnnounce(torrent.createAnnounceableInformation(), this, event);
+    } catch (IOException e) {
+      logger.warn("unable to force announce torrent {}", torrent);
+      logger.debug("", e);
+    }
+  }
+
+  /**
+   * Removes specified torrent from storage.
+   *
+   * @param torrentHash specified torrent hash
+   */
+
+  //移除任务
+  public void removeTorrent(String torrentHash) {
+    logger.debug("Stopping seeding " + torrentHash);
+    final Pair<SharedTorrent, LoadedTorrent> torrents = torrentsStorage.remove(torrentHash);
+
+    SharedTorrent torrent = torrents.first();
+    if (torrent != null) {
+      torrent.setClientState(ClientState.DONE);
+      torrent.closeFully();
+    }
+    List<SharingPeer> peers = getPeersForTorrent(torrentHash);
+    for (SharingPeer peer : peers) {
+      peer.unbind(true);
+    }
+    sendStopEvent(torrents.second(), torrentHash);
+  }
+
+  private void sendStopEvent(LoadedTorrent loadedTorrent, String torrentHash) {
+    if (loadedTorrent == null) {
+      logger.info("Announceable torrent {} not found in storage after unsuccessful download attempt", torrentHash);
+      return;
+    }
+    forceAnnounceAndLogError(loadedTorrent, STOPPED);
+  }
+
+  /**
+   * set specified announce interval between requests to the tracker
+   *
+   * @param announceInterval announce interval in seconds
+   */
+  public void setAnnounceInterval(final int announceInterval) {
+    announce.setAnnounceInterval(announceInterval);
+  }
+
+  /**
+   * Return the torrent this client is exchanging on.
+   */
+  public Collection<SharedTorrent> getTorrents() {
+    return this.torrentsStorage.activeTorrents();
+  }
+
+  @SuppressWarnings("unused")
+  public URI getDefaultTrackerURI() {
+    return announce.getDefaultTrackerURI();
+  }
+
+  /**
+   * Returns the set of known peers.
+   */
+  public Set<SharingPeer> getPeers() {
+    return new HashSet<SharingPeer>(this.peersStorage.getSharingPeers());
+  }
+
+  public void setMaxInConnectionsCount(int maxConnectionsCount) {
+    this.myInConnectionAllower.setMyMaxConnectionCount(maxConnectionsCount);
+  }
+
+  /**
+   * set ups new receive buffer size, that will be applied to all new connections.
+   * If value is equal or less, than zero, then method doesn't have effect
+   *
+   * @param newSize new size
+   */
+  public void setReceiveBufferSize(int newSize) {
+    myReceiveBufferSize.set(newSize);
+  }
+
+  /**
+   * set ups new send buffer size, that will be applied to all new connections.
+   * If value is equal or less, than zero, then method doesn't have effect
+   *
+   * @param newSize new size
+   */
+  public void setSendBufferSize(int newSize) {
+    mySendBufferSize.set(newSize);
+  }
+
+  public void setMaxOutConnectionsCount(int maxConnectionsCount) {
+    this.myOutConnectionAllower.setMyMaxConnectionCount(maxConnectionsCount);
+  }
+
+  /**
+   * Runs client instance and starts announcing, seeding and downloading of all torrents from storage
+   *
+   * @param bindAddresses list of addresses which are used for sending to the tracker. Current client
+   *                      must be available for other peers on the addresses
+   * @throws IOException if any io error occurs
+   */
+  public void start(final InetAddress... bindAddresses) throws IOException {
+    start(bindAddresses, Constants.DEFAULT_ANNOUNCE_INTERVAL_SEC, null, new SelectorFactoryImpl());
+  }
+
+  /**
+   * Runs client instance and starts announcing, seeding and downloading of all torrents from storage
+   *
+   * @param bindAddresses     list of addresses which are used for sending to the tracker. Current client
+   *                          must be available for other peers on the addresses
+   * @param defaultTrackerURI default tracker address.
+   *                          All torrents will be announced not only on the trackers from metadata file but also to this tracker
+   * @throws IOException if any io error occurs
+   */
+  public void start(final InetAddress[] bindAddresses, final URI defaultTrackerURI) throws IOException {
+    start(bindAddresses, Constants.DEFAULT_ANNOUNCE_INTERVAL_SEC, defaultTrackerURI, new SelectorFactoryImpl());
+  }
+
+  public Peer[] getSelfPeers(final InetAddress[] bindAddresses) throws UnsupportedEncodingException {
+    Peer self = peersStorage.getSelf();
+
+    if (self == null) {
+      return new Peer[0];
+    }
+
+    Peer[] result = new Peer[bindAddresses.length];
+    for (int i = 0; i < bindAddresses.length; i++) {
+      final InetAddress bindAddress = bindAddresses[i];
+      final Peer peer = new Peer(new InetSocketAddress(bindAddress.getHostAddress(), self.getPort()));
+      peer.setTorrentHash(self.getHexInfoHash());
+      //if we have more, that one bind address, then only for first set self peer id. For other generate it
+      if (i == 0) {
+        peer.setPeerId(self.getPeerId());
+      } else {
+        final String id = CommunicationManager.BITTORRENT_ID_PREFIX + UUID.randomUUID().toString().split("-")[4];
+        byte[] idBytes = id.getBytes(Constants.BYTE_ENCODING);
+        peer.setPeerId(ByteBuffer.wrap(idBytes));
+      }
+      result[i] = peer;
+    }
+    return result;
+  }
+
+  /**
+   * Runs client instance and starts announcing, seeding and downloading of all torrents from storage
+   *
+   * @param bindAddresses       list of addresses which are used for sending to the tracker. Current client
+   *                            must be available for other peers on the addresses
+   * @param announceIntervalSec default announce interval. This interval can be override by tracker
+   * @param defaultTrackerURI   default tracker address.
+   *                            All torrents will be announced not only on the trackers from metadata file but also to this tracker
+   * @param selectorFactory     factory for creating {@link java.nio.channels.Selector} instance.
+   * @throws IOException if any io error occurs
+   */
+  public void start(final InetAddress[] bindAddresses,
+                    final int announceIntervalSec,
+                    final URI defaultTrackerURI,
+                    final SelectorFactory selectorFactory) throws IOException {
+    start(bindAddresses, announceIntervalSec, defaultTrackerURI, selectorFactory,
+            new FirstAvailableChannel(6881, 6889));
+  }
+
+  public void start(final InetAddress[] bindAddresses,
+                    final int announceIntervalSec,
+                    final URI defaultTrackerURI,
+                    final SelectorFactory selectorFactory,
+                    final ServerChannelRegister serverChannelRegister) throws IOException {
+    this.myConnectionManager = new ConnectionManager(
+            this,
+            new SystemTimeService(),
+            myInConnectionAllower,
+            myOutConnectionAllower,
+            selectorFactory,
+            mySendBufferSize,
+            myReceiveBufferSize);
+    this.setSocketConnectionTimeout(DEFAULT_SOCKET_CONNECTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+    try {
+      this.myConnectionManager.initAndRunWorker(serverChannelRegister);
+    } catch (IOException e) {
+      LoggerUtils.errorAndDebugDetails(logger, "error in initialization server channel", e);
+      this.stop();
+      return;
+    }
+    final String id = CommunicationManager.BITTORRENT_ID_PREFIX + UUID.randomUUID().toString().split("-")[4];
+    byte[] idBytes = id.getBytes(Constants.BYTE_ENCODING);
+    Peer self = new Peer(new InetSocketAddress(myConnectionManager.getBindPort()), ByteBuffer.wrap(idBytes));
+    peersStorage.setSelf(self);
+    logger.info("BitTorrent client [{}] started and " +
+                    "listening at {}:{}...",
+            new Object[]{
+                    self.getShortHexPeerId(),
+                    self.getIp(),
+                    self.getPort()
+            });
+
+    announce.start(defaultTrackerURI, this, getSelfPeers(bindAddresses), announceIntervalSec);
+    this.stop.set(false);
+
+    myStarted = true;
+  }
+
+  /**
+   * Immediately but gracefully stop this client.
+   */
+  public void stop() {
+    this.stop(60, TimeUnit.SECONDS);
+  }
+
+  void stop(int timeout, TimeUnit timeUnit) {
+    boolean wasStopped = this.stop.getAndSet(true);
+    if (wasStopped) return;
+
+    if (!myStarted)
+      return;
+
+    this.myConnectionManager.close();
+
+    logger.trace("try stop announce thread...");
+
+    this.announce.stop();
+
+    logger.trace("announce thread is stopped");
+
+    for (SharedTorrent torrent : this.torrentsStorage.activeTorrents()) {
+      logger.trace("try close torrent {}", torrent);
+      torrent.closeFully();
+      if (torrent.isFinished()) {
+        torrent.setClientState(ClientState.DONE);
+      } else {
+        torrent.setClientState(ClientState.ERROR);
+      }
+    }
+
+    logger.debug("Closing all remaining peer connections...");
+    for (SharingPeer peer : this.peersStorage.getSharingPeers()) {
+      peer.unbind(true);
+    }
+
+    torrentsStorage.clear();
+    logger.info("BitTorrent client signing off.");
+  }
+
+  public void setCleanupTimeout(int timeout, TimeUnit timeUnit) throws IllegalStateException {
+    ConnectionManager connectionManager = this.myConnectionManager;
+    if (connectionManager == null) {
+      throw new IllegalStateException("connection manager is null");
+    }
+    connectionManager.setCleanupTimeout(timeUnit.toMillis(timeout));
+  }
+
+  public void setSocketConnectionTimeout(int timeout, TimeUnit timeUnit) throws IllegalStateException {
+    ConnectionManager connectionManager = this.myConnectionManager;
+    if (connectionManager == null) {
+      throw new IllegalStateException("connection manager is null");
+    }
+    connectionManager.setSocketConnectionTimeout(timeUnit.toMillis(timeout));
+  }
+
+  /**
+   * Tells whether we are a seed for the torrent we're sharing.
+   */
+  public boolean isSeed(String hexInfoHash) {
+    SharedTorrent t = this.torrentsStorage.getTorrent(hexInfoHash);
+    return t != null && t.isComplete();
+  }
+
+  public List<SharingPeer> getPeersForTorrent(String torrentHash) {
+    if (torrentHash == null) return new ArrayList<SharingPeer>();
+
+    List<SharingPeer> result = new ArrayList<SharingPeer>();
+    for (SharingPeer sharingPeer : peersStorage.getSharingPeers()) {
+      if (torrentHash.equals(sharingPeer.getHexInfoHash())) {
+        result.add(sharingPeer);
+      }
+    }
+    return result;
+  }
+
+  public boolean isRunning() {
+    return myStarted;
+  }
+
+  private Collection<SharingPeer> getConnectedPeers() {
+    Set<SharingPeer> result = new HashSet<SharingPeer>();
+    for (SharingPeer peer : this.peersStorage.getSharingPeers()) {
+      if (peer.isConnected()) {
+        result.add(peer);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * @param hash specified torrent hash
+   * @return true if storage contains specified torrent. False otherwise
+   * @see TorrentsStorage#hasTorrent
+   */
+  @SuppressWarnings("unused")
+  public boolean containsTorrentWithHash(String hash) {
+    return torrentsStorage.hasTorrent(hash);
+  }
+
+  @Override
+  public PeersStorage getPeersStorage() {
+    return peersStorage;
+  }
+
+  @Override
+  public TorrentsStorage getTorrentsStorage() {
+    return torrentsStorage;
+  }
+
+  @Override
+  public ExecutorService getExecutor() {
+    return myExecutorService;
+  }
+
+  public ExecutorService getPieceValidatorExecutor() {
+    return myPieceValidatorExecutor;
+  }
+
+  @Override
+  public ConnectionListener newChannelListener() {
+    return new StateChannelListener(this);
+  }
+
+  @Override
+  public SharingPeer createSharingPeer(String host,
+                                       int port,
+                                       ByteBuffer peerId,
+                                       SharedTorrent torrent,
+                                       ByteChannel channel,
+                                       String clientIdentifier,
+                                       int clientVersion) {
+    return new SharingPeer(host, port, peerId, torrent, getConnectionManager(), this, channel, clientIdentifier, clientVersion);
+  }
+
+  @Override
+  public TorrentLoader getTorrentLoader() {
+    return myTorrentLoader;
+  }
+
+
+  /** AnnounceResponseListener handler(s). **********************************/
+
+  /**
+   * Handle an announce response event.
+   *
+   * @param interval   The announce interval requested by the tracker.
+   * @param complete   The number of seeders on this torrent.
+   * @param incomplete The number of leechers on this torrent.
+   */
+  @Override
+  public void handleAnnounceResponse(int interval, int complete, int incomplete, String hexInfoHash) {
+    final SharedTorrent sharedTorrent = this.torrentsStorage.getTorrent(hexInfoHash);
+    if (sharedTorrent != null) {
+      sharedTorrent.setSeedersCount(complete);
+      sharedTorrent.setLastAnnounceTime(System.currentTimeMillis());
+    }
+    setAnnounceInterval(interval);
+  }
+
+  /**
+   * Handle the discovery of new peers.
+   *
+   * @param peers The list of peers discovered (from the announce response or
+   *              any other means like DHT/PEX, etc.).
+   */
+  @Override
+  public void handleDiscoveredPeers(List<Peer> peers, String hexInfoHash) {
+
+    if (peers.size() == 0) return;
+
+    SharedTorrent torrent = torrentsStorage.getTorrent(hexInfoHash);
+
+    if (torrent != null && torrent.isFinished()) return;
+
+    final LoadedTorrent announceableTorrent = torrentsStorage.getLoadedTorrent(hexInfoHash);
+    if (announceableTorrent == null) {
+      logger.info("announceable torrent {} is not found in storage. Maybe it was removed", hexInfoHash);
+      return;
+    }
+
+    if (announceableTorrent.getPieceStorage().isFinished()) return;
+
+    logger.debug("Got {} peer(s) ({}) for {} in tracker response", new Object[]{peers.size(),
+            Arrays.toString(peers.toArray()), hexInfoHash});
+
+    Map<PeerUID, Peer> uniquePeers = new HashMap<PeerUID, Peer>();
+    for (Peer peer : peers) {
+      final PeerUID peerUID = new PeerUID(peer.getAddress(), hexInfoHash);
+      if (uniquePeers.containsKey(peerUID)) continue;
+      uniquePeers.put(peerUID, peer);
+    }
+
+    for (Map.Entry<PeerUID, Peer> e : uniquePeers.entrySet()) {
+
+      PeerUID peerUID = e.getKey();
+      Peer peer = e.getValue();
+      boolean alreadyConnectedToThisPeer = peersStorage.getSharingPeer(peerUID) != null;
+
+      if (alreadyConnectedToThisPeer) {
+        logger.debug("skipping peer {}, because we already connected to this peer", peer);
+        continue;
+      }
+
+      ConnectionListener connectionListener = new OutgoingConnectionListener(
+              this,
+              announceableTorrent.getTorrentHash(),
+              peer.getIp(),
+              peer.getPort());
+
+      logger.debug("trying to connect to the peer {}", peer);
+
+      boolean connectTaskAdded = this.myConnectionManager.offerConnect(
+              new ConnectTask(peer.getIp(),
+                      peer.getPort(),
+                      connectionListener,
+                      new SystemTimeService().now(),
+                      Constants.DEFAULT_CONNECTION_TIMEOUT_MILLIS), 1, TimeUnit.SECONDS);
+      if (!connectTaskAdded) {
+        logger.info("can not connect to peer {}. Unable to add connect task to connection manager", peer);
+      }
+    }
+  }
+
+  /**
+   * PeerActivityListener handler(s). *************************************
+   */
+
+  @Override
+  public void handlePeerChoked(SharingPeer peer) { /* Do nothing */ }
+
+  @Override
+  public void handlePeerReady(SharingPeer peer) { /* Do nothing */ }
+
+  @Override
+  public void handlePieceAvailability(SharingPeer peer,
+                                      Piece piece) { /* Do nothing */ }
+
+  @Override
+  public void handleBitfieldAvailability(SharingPeer peer,
+                                         BitSet availablePieces) { /* Do nothing */ }
+
+  @Override
+  public void handlePieceSent(SharingPeer peer,
+                              Piece piece) { /* Do nothing */ }
+
+  /**
+   * Piece download completion handler.
+   * <p/>
+   * <p>
+   * When a piece is completed, and valid, we announce to all connected peers
+   * that we now have this piece.
+   * </p>
+   * <p/>
+   * <p>
+   * We use this handler to identify when all of the pieces have been
+   * downloaded. When that's the case, we can start the seeding period, if
+   * any.
+   * </p>
+   *
+   * @param peer  The peer we got the piece from.
+   * @param piece The piece in question.
+   */
+  @Override
+  public void handlePieceCompleted(final SharingPeer peer, final Piece piece)
+          throws IOException {
+    final SharedTorrent torrent = peer.getTorrent();
+    final String torrentHash = torrent.getHexInfoHash();
+    try {
+      final Future<?> validationFuture = myPieceValidatorExecutor.submit(new Runnable() {
+        @Override
+        public void run() {
+          validatePieceAsync(torrent, piece, torrentHash, peer);
+        }
+      });
+      torrent.markCompletedAndAddValidationFuture(piece, validationFuture);
+    } catch (RejectedExecutionException e) {
+      torrent.markUncompleted(piece);
+      LoggerUtils.warnWithMessageAndDebugDetails(logger, "Unable to submit validation task for torrent {}", torrentHash, e);
+    }
+  }
+
+  private void validatePieceAsync(final SharedTorrent torrent, final Piece piece, String torrentHash, SharingPeer peer) {
+    try {
+      synchronized (piece) {
+
+        if (piece.isValid()) return;
+
+        piece.validate(torrent, piece);
+        if (piece.isValid()) {
+          torrent.notifyPieceDownloaded(piece, peer);
+          piece.finish();
+          // Send a HAVE message to all connected peers, which don't have the piece
+          PeerMessage have = PeerMessage.HaveMessage.craft(piece.getIndex());
+          for (SharingPeer remote : getConnectedPeers()) {
+            if (remote.getTorrent().getHexInfoHash().equals(torrentHash) &&
+                    !remote.getAvailablePieces().get(piece.getIndex()))
+              remote.send(have);
+          }
+          peer.pieceDownloaded();
+
+          final boolean isTorrentComplete;
+          synchronized (torrent) {
+            torrent.removeValidationFuture(piece);
+
+            boolean isCurrentPeerSeeder = peer.getAvailablePieces().cardinality() == torrent.getPieceCount();
+            //if it's seeder we will send not interested message when we download full file
+            if (!isCurrentPeerSeeder) {
+              if (torrent.isAllPiecesOfPeerCompletedAndValidated(peer)) {
+                peer.notInteresting();
+              }
+            }
+
+            isTorrentComplete = torrent.isComplete();
+
+            if (isTorrentComplete) {
+              logger.info("Download of {} complete.", torrent.getDirectoryName());
+
+              torrent.finish();
+            }
+          }
+
+          if (isTorrentComplete) {
+
+            LoadedTorrent announceableTorrent = torrentsStorage.getLoadedTorrent(torrentHash);
+
+            if (announceableTorrent == null) return;
+
+            AnnounceableInformation announceableInformation = announceableTorrent.createAnnounceableInformation();
+
+            if (!TorrentUtils.isTrackerLessInfo(announceableInformation)) {
+              try {
+                announce.getCurrentTrackerClient(announceableInformation)
+                        .announceAllInterfaces(COMPLETED, true, announceableInformation);
+              } catch (AnnounceException e) {
+                logger.debug("unable to announce torrent {} on tracker {}", torrent, torrent.getAnnounce());
+              }
+            }
+
+            for (SharingPeer remote : getPeersForTorrent(torrentHash)) {
+              remote.notInteresting();
+            }
+
+          }
+        } else {
+          torrent.markUncompleted(piece);
+          logger.info("Downloaded piece #{} from {} was not valid ;-(. Trying another peer", piece.getIndex(), peer);
+          peer.getPoorlyAvailablePieces().set(piece.getIndex());
+        }
+      }
+    } catch (Throwable e) {
+      torrent.markUncompleted(piece);
+      logger.warn("unhandled exception in piece {} validation task", e);
+    }
+    torrent.handlePeerReady(peer);
+  }
+
+  @Override
+  public void handlePeerDisconnected(SharingPeer peer) {
+    Peer p = new Peer(peer.getIp(), peer.getPort());
+    p.setPeerId(peer.getPeerId());
+    p.setTorrentHash(peer.getHexInfoHash());
+    logger.trace("Peer {} disconnected, [{}/{}].",
+            new Object[]{
+                    peer,
+                    getConnectedPeers().size(),
+                    this.peersStorage.getSharingPeers().size()
+            });
+    PeerUID peerUID = new PeerUID(peer.getAddress(), peer.getHexInfoHash());
+    peersStorage.removeSharingPeer(peerUID);
+  }
+
+  @Override
+  public void afterPeerRemoved(SharingPeer peer) {
+    logger.trace("disconnected peer " + peer);
+    torrentsStorage.peerDisconnected(peer.getHexInfoHash());
+  }
+
+  @Override
+  public void handleIOException(SharingPeer peer, IOException ioe) {
+    logger.debug("I/O problem occured when reading or writing piece data for peer {}: {}.", peer, ioe.getMessage());
+
+    peer.unbind(true);
+  }
+
+  @Override
+  public void handleNewPeerConnected(SharingPeer peer) {
+    //do nothing
+  }
+
+  public ConnectionManager getConnectionManager() throws IllegalStateException {
+    ConnectionManager connectionManager = this.myConnectionManager;
+    if (connectionManager == null) {
+      throw new IllegalStateException("connection manager is null");
+    }
+    return connectionManager;
+  }
+
+  public boolean hasStop(){
+    return stop.get();
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Context.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Context.java
new file mode 100644
index 0000000..24170e5
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Context.java
@@ -0,0 +1,29 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.network.ChannelListenerFactory;
+
+import java.util.concurrent.ExecutorService;
+
+public interface Context extends SharingPeerFactory, ChannelListenerFactory {
+
+  /**
+   * @return single instance of peers storage
+   */
+  PeersStorage getPeersStorage();
+
+  /**
+   * @return single instance of torrents storage
+   */
+  TorrentsStorage getTorrentsStorage();
+
+  /**
+   * @return executor for handling incoming messages
+   */
+  ExecutorService getExecutor();
+
+  /**
+   * @return single instance for load torrents
+   */
+  TorrentLoader getTorrentLoader();
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/EventDispatcher.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/EventDispatcher.java
new file mode 100644
index 0000000..f031219
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/EventDispatcher.java
@@ -0,0 +1,80 @@
+package com.turn.ttorrent.client;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class EventDispatcher {
+
+  private final List<TorrentListener> listeners;
+  private final TorrentListener notifyer;
+
+  public EventDispatcher() {
+    this.listeners = new CopyOnWriteArrayList<TorrentListener>();
+    this.notifyer = createNotifyer();
+  }
+
+  private TorrentListener createNotifyer() {
+    return new TorrentListener() {
+      @Override
+      public void peerConnected(PeerInformation peerInformation) {
+        for (TorrentListener listener : listeners) {
+          listener.peerConnected(peerInformation);
+        }
+      }
+
+      @Override
+      public void peerDisconnected(PeerInformation peerInformation) {
+        for (TorrentListener listener : listeners) {
+          listener.peerDisconnected(peerInformation);
+        }
+      }
+
+      @Override
+      public void pieceDownloaded(PieceInformation pieceInformation, PeerInformation peerInformation) {
+        for (TorrentListener listener : listeners) {
+          listener.pieceDownloaded(pieceInformation, peerInformation);
+        }
+      }
+
+      @Override
+      public void downloadComplete() {
+        for (TorrentListener listener : listeners) {
+          listener.downloadComplete();
+        }
+      }
+
+      @Override
+      public void pieceReceived(PieceInformation pieceInformation, PeerInformation peerInformation) {
+        for (TorrentListener listener : listeners) {
+          listener.pieceReceived(pieceInformation, peerInformation);
+        }
+      }
+
+      @Override
+      public void downloadFailed(Throwable cause) {
+        for (TorrentListener listener : listeners) {
+          listener.downloadFailed(cause);
+        }
+      }
+
+      @Override
+      public void validationComplete(int validpieces, int totalpieces) {
+        for (TorrentListener listener : listeners) {
+          listener.validationComplete(validpieces, totalpieces);
+        }
+      }
+    };
+  }
+
+  TorrentListener multicaster() {
+    return notifyer;
+  }
+
+  public boolean removeListener(TorrentListener listener) {
+    return listeners.remove(listener);
+  }
+
+  public void addListener(TorrentListener listener) {
+    listeners.add(listener);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/FileMetadataProvider.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/FileMetadataProvider.java
new file mode 100644
index 0000000..10b24dd
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/FileMetadataProvider.java
@@ -0,0 +1,24 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentParser;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileMetadataProvider implements TorrentMetadataProvider {
+
+  private final String filePath;
+
+  public FileMetadataProvider(String filePath) {
+    this.filePath = filePath;
+  }
+
+  @NotNull
+  @Override
+  public TorrentMetadata getTorrentMetadata() throws IOException {
+    File file = new File(filePath);
+    return new TorrentParser().parseFromFile(file);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Handshake.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Handshake.java
new file mode 100644
index 0000000..229681c
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Handshake.java
@@ -0,0 +1,190 @@
+/**
+ * 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.client;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.common.TorrentHash;
+import com.turn.ttorrent.common.TorrentUtils;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.text.ParseException;
+
+
+/**
+ * Peer handshake handler.
+ *
+ * @author mpetazzoni
+ * @comments rpilyushin
+ *
+ */
+
+/**
+ * Represents a BitTorrent handshake message.
+ * This class encapsulates the structure and parsing logic for the handshake
+ * that is exchanged between peers when establishing a connection in the
+ * BitTorrent protocol.
+ */
+
+public class Handshake implements TorrentHash {
+
+  // BitTorrent protocol identifier as specified by the BitTorrent specification.
+  public static final String BITTORRENT_PROTOCOL_IDENTIFIER = "BitTorrent protocol";
+  // Base length for a handshake message without the protocol identifier.
+  public static final int BASE_HANDSHAKE_LENGTH = 49;
+
+  // ByteBuffer to store the full handshake data.
+  private ByteBuffer data;
+  // ByteBuffer to store the torrent info hash.
+  private ByteBuffer infoHash;
+  // ByteBuffer to store the peer ID.
+  private ByteBuffer peerId;
+
+  // String to store an identifier for the torrent, not used in the actual handshake message.
+  private String torrentIdentifier;
+
+  // The length of the protocol identifier string in this handshake.
+  private int myPstrlen;
+
+  // Private constructor for internal use to set up the handshake object.
+  private Handshake(ByteBuffer data, ByteBuffer infoHash,
+                    ByteBuffer peerId) {
+    this.data = data;
+    this.data.rewind(); // Rewind the buffer to the start for reading.
+
+    this.infoHash = infoHash;
+    this.peerId = peerId;
+  }
+
+  // Returns the raw handshake data as a ByteBuffer.
+  public ByteBuffer getData() {
+    return this.data;
+  }
+
+  // Returns the info hash as a byte array.
+  public byte[] getInfoHash() {
+    return this.infoHash.array();
+  }
+
+  // Returns a hexadecimal string representation of the info hash.
+  public String getHexInfoHash() {
+    return TorrentUtils.byteArrayToHexString(getInfoHash());
+  }
+
+  // Returns the peer ID as a byte array.
+  public byte[] getPeerId() {
+    return this.peerId.array();
+  }
+
+  // Parses a ByteBuffer into a Handshake object, validating the structure of the handshake.
+  public static Handshake parse(ByteBuffer buffer)
+          throws ParseException, UnsupportedEncodingException {
+    // Get the length of the protocol identifier from the first byte.
+    int pstrlen = Byte.valueOf(buffer.get()).intValue();
+    // Check that the length is correct given the remaining data.
+    if (pstrlen < 0 ||
+            buffer.remaining() != BASE_HANDSHAKE_LENGTH + pstrlen - 1) {
+      throw new ParseException("Incorrect handshake message length " +
+              "(pstrlen=" + pstrlen + ") !", 0);
+    }
+
+    // Parse the protocol identifier and validate it.
+    byte[] pstr = new byte[pstrlen];
+    buffer.get(pstr);
+
+    if (!Handshake.BITTORRENT_PROTOCOL_IDENTIFIER.equals(
+            new String(pstr, Constants.BYTE_ENCODING))) {
+      throw new ParseException("Invalid protocol identifier!", 1);
+    }
+
+    // Skip over the reserved bytes, which are not currently used.
+    byte[] reserved = new byte[8];
+    buffer.get(reserved);
+
+    // Parse the info hash and peer ID from the buffer.
+    byte[] infoHash = new byte[20];
+    buffer.get(infoHash);
+    byte[] peerId = new byte[20];
+    buffer.get(peerId);
+    // Return a new handshake object with the parsed data.
+    return new Handshake(buffer, ByteBuffer.wrap(infoHash),
+            ByteBuffer.wrap(peerId));
+  }
+
+  // Additional overloaded parse method which also sets the torrent identifier.
+  public static Handshake parse(ByteBuffer buffer, String torrentIdentifier) throws UnsupportedEncodingException, ParseException {
+    Handshake hs = Handshake.parse(buffer);
+    hs.setTorrentIdentifier(torrentIdentifier);
+    return hs;
+  }
+
+  // Additional overloaded parse method which also sets the protocol identifier length.
+  public static Handshake parse(ByteBuffer buffer, int pstrlen) throws UnsupportedEncodingException, ParseException {
+    Handshake hs = Handshake.parse(buffer);
+    hs.myPstrlen = pstrlen;
+    return hs;
+  }
+
+  // Method to craft a new handshake message given a torrent info hash and peer ID.
+  public static Handshake craft(byte[] torrentInfoHash, byte[] clientPeerId) {
+    try {
+      // Allocate a ByteBuffer with the size of the handshake message.
+      ByteBuffer buffer = ByteBuffer.allocate(
+              Handshake.BASE_HANDSHAKE_LENGTH +
+                      Handshake.BITTORRENT_PROTOCOL_IDENTIFIER.length());
+
+      byte[] reserved = new byte[8]; // Reserved bytes, not used.
+      ByteBuffer infoHash = ByteBuffer.wrap(torrentInfoHash);
+      ByteBuffer peerId = ByteBuffer.wrap(clientPeerId);
+
+      // Construct the handshake message in the buffer.
+      buffer.put((byte) Handshake
+              .BITTORRENT_PROTOCOL_IDENTIFIER.length());
+      buffer.put(Handshake
+              .BITTORRENT_PROTOCOL_IDENTIFIER.getBytes(Constants.BYTE_ENCODING));
+      buffer.put(reserved);
+      buffer.put(infoHash);
+      buffer.put(peerId);
+
+      // Return a new handshake object with the constructed message.
+      return new Handshake(buffer, infoHash, peerId);
+    } catch (UnsupportedEncodingException uee) {
+      return null; // In case the encoding is not supported, return null.
+    }
+  }
+
+  // Additional method to craft a handshake message with the torrent identifier set.
+  public static Handshake parse(byte[] torrentInfoHash, byte[] clientPeerId, String torrentIdentifier) throws UnsupportedEncodingException, ParseException {
+    Handshake hs = Handshake.craft(torrentInfoHash, clientPeerId);
+    hs.setTorrentIdentifier(torrentIdentifier);
+    return hs;
+  }
+
+  // Sets the torrent identifier for this handshake.
+  public void setTorrentIdentifier(String torrentIdentifier) {
+    this.torrentIdentifier = torrentIdentifier;
+  }
+
+  // Gets the protocol identifier length for this handshake.
+  public int getPstrlen() {
+    return myPstrlen;
+  }
+
+  // Gets the torrent identifier.
+  public String getTorrentIdentifier() {
+    return torrentIdentifier;
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/LoadedTorrent.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/LoadedTorrent.java
new file mode 100644
index 0000000..765a56c
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/LoadedTorrent.java
@@ -0,0 +1,45 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.client.storage.PieceStorage;
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.TorrentHash;
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentStatistic;
+import org.jetbrains.annotations.NotNull;
+
+public interface LoadedTorrent {
+
+  /**
+   * @return {@link PieceStorage} where stored available pieces
+   */
+  PieceStorage getPieceStorage();
+
+  /**
+   * @return {@link TorrentMetadata} instance
+   * @throws IllegalStateException if unable to fetch metadata from source
+   *                               (e.g. source is .torrent file and it was deleted manually)
+   */
+  TorrentMetadata getMetadata() throws IllegalStateException;
+
+  /**
+   * @return new instance of {@link AnnounceableInformation} for announce this torrent to the tracker
+   */
+  @NotNull
+  AnnounceableInformation createAnnounceableInformation();
+
+  /**
+   * @return {@link TorrentStatistic} instance related with this torrent
+   */
+  TorrentStatistic getTorrentStatistic();
+
+  /**
+   * @return hash of this torrent
+   */
+  TorrentHash getTorrentHash();
+
+  /**
+   * @return related {@link EventDispatcher}
+   */
+  EventDispatcher getEventDispatcher();
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/LoadedTorrentImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/LoadedTorrentImpl.java
new file mode 100644
index 0000000..37a6a7a
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/LoadedTorrentImpl.java
@@ -0,0 +1,88 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.client.storage.PieceStorage;
+import com.turn.ttorrent.common.*;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+public class LoadedTorrentImpl implements LoadedTorrent {
+
+  private final TorrentStatistic torrentStatistic;
+  private final TorrentHash torrentHash;
+  private final List<List<String>> announceUrls;
+  private final String announce;
+  private final PieceStorage pieceStorage;
+  private final TorrentMetadataProvider metadataProvider;
+  private final EventDispatcher eventDispatcher;
+
+  LoadedTorrentImpl(TorrentStatistic torrentStatistic,
+                    TorrentMetadataProvider metadataProvider,
+                    TorrentMetadata torrentMetadata,
+                    PieceStorage pieceStorage,
+                    EventDispatcher eventDispatcher) {
+    this.torrentStatistic = torrentStatistic;
+    this.metadataProvider = metadataProvider;
+    this.eventDispatcher = eventDispatcher;
+    torrentHash = new ImmutableTorrentHash(torrentMetadata.getInfoHash());
+    if (torrentMetadata.getAnnounceList() != null) {
+      this.announceUrls = Collections.unmodifiableList(torrentMetadata.getAnnounceList());
+    } else {
+      this.announceUrls = Collections.singletonList(Collections.singletonList(torrentMetadata.getAnnounce()));
+    }
+    this.announce = torrentMetadata.getAnnounce();
+    this.pieceStorage = pieceStorage;
+  }
+
+  @Override
+  public PieceStorage getPieceStorage() {
+    return pieceStorage;
+  }
+
+  @Override
+  public TorrentMetadata getMetadata() {
+    try {
+      return metadataProvider.getTorrentMetadata();
+    } catch (IOException e) {
+      throw new IllegalStateException("Unable to fetch torrent metadata from metadata provider: " + metadataProvider, e);
+    }
+  }
+
+  @Override
+  public TorrentStatistic getTorrentStatistic() {
+    return torrentStatistic;
+  }
+
+  @Override
+  @NotNull
+  public AnnounceableInformation createAnnounceableInformation() {
+    return new AnnounceableInformationImpl(
+            torrentStatistic.getUploadedBytes(),
+            torrentStatistic.getDownloadedBytes(),
+            torrentStatistic.getLeftBytes(),
+            torrentHash,
+            announceUrls,
+            announce
+    );
+  }
+
+  @Override
+  public TorrentHash getTorrentHash() {
+    return torrentHash;
+  }
+
+  @Override
+  public EventDispatcher getEventDispatcher() {
+    return eventDispatcher;
+  }
+
+  @Override
+  public String toString() {
+    return "LoadedTorrentImpl{" +
+            "piece storage='" + pieceStorage + '\'' +
+            ", metadata provider='" + metadataProvider + '\'' +
+            '}';
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeerInformation.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeerInformation.java
new file mode 100644
index 0000000..5b02c8d
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeerInformation.java
@@ -0,0 +1,27 @@
+package com.turn.ttorrent.client;
+
+import java.net.InetSocketAddress;
+
+public interface PeerInformation {
+
+  /**
+   * @return {@link InetSocketAddress} of remote peer
+   */
+  InetSocketAddress getAddress();
+
+  /**
+   * @return id of current peer which the peers sent in the handshake
+   */
+  byte[] getId();
+
+  /**
+   * @return client identifier of current peer
+   */
+  String getClientIdentifier();
+
+  /**
+   * @return client version of current peer
+   */
+  int getClientVersion();
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeersStorage.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeersStorage.java
new file mode 100644
index 0000000..cc6239a
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PeersStorage.java
@@ -0,0 +1,49 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.PeerUID;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+public class PeersStorage {
+
+  private volatile Peer self = null;
+  private final ConcurrentHashMap<PeerUID, SharingPeer> connectedSharingPeers;
+
+  public PeersStorage() {
+    this.connectedSharingPeers = new ConcurrentHashMap<PeerUID, SharingPeer>();
+  }
+
+  public Peer getSelf() {
+    return self;
+  }
+
+  public void setSelf(Peer self) {
+    this.self = self;
+  }
+
+  public SharingPeer putIfAbsent(PeerUID peerId, SharingPeer sharingPeer) {
+    return connectedSharingPeers.putIfAbsent(peerId, sharingPeer);
+  }
+
+  public SharingPeer removeSharingPeer(PeerUID peerId) {
+    return connectedSharingPeers.remove(peerId);
+  }
+
+  public SharingPeer getSharingPeer(PeerUID peerId) {
+    return connectedSharingPeers.get(peerId);
+  }
+
+  public void removeSharingPeer(SharingPeer peer) {
+    connectedSharingPeers.values().remove(peer);
+  }
+
+  public Collection<SharingPeer> getSharingPeers() {
+    return new ArrayList<SharingPeer>(connectedSharingPeers.values());
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Piece.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Piece.java
new file mode 100644
index 0000000..d966813
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/Piece.java
@@ -0,0 +1,294 @@
+/**
+ * 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.client;
+
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.client.storage.PieceStorage;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.TorrentUtils;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+
+/**
+ * A torrent piece.
+ *
+ * <p>
+ * This class represents a torrent piece. Torrents are made of pieces, which
+ * are in turn made of blocks that are exchanged using the peer protocol.
+ * The piece length is defined at the torrent level, but the last piece that
+ * makes the torrent might be smaller.
+ * </p>
+ *
+ * <p>
+ * If the torrent has multiple files, pieces can spread across file boundaries.
+ * The TorrentByteStorage abstracts this problem to give Piece objects the
+ * impression of a contiguous, linear byte storage.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public class Piece implements Comparable<Piece>, PieceInformation {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(Piece.class);
+
+  private final PieceStorage pieceStorage;
+  private final int index;
+  private final long length;
+  private final byte[] hash;
+
+  private volatile boolean valid;
+  private int seen;
+  private ByteBuffer data;
+
+  /**
+   * Initialize a new piece in the byte bucket.
+   *
+   * @param pieceStorage  The underlying piece storage bucket.
+   * @param index   This piece index in the torrent.
+   * @param length  This piece length, in bytes.
+   * @param hash    This piece 20-byte SHA1 hash sum.
+   */
+  public Piece(PieceStorage pieceStorage, int index, long length, byte[] hash) {
+    this.pieceStorage = pieceStorage;
+    this.index = index;
+    this.length = length;
+    this.hash = hash;
+
+    // Piece is considered invalid until first check.
+    this.valid = false;
+
+    // Piece start unseen
+    this.seen = 0;
+
+    this.data = null;
+  }
+
+  @Override
+  public int getSize() {
+    return (int)length;
+  }
+
+  /**
+   * Tells whether this piece's data is valid or not.
+   */
+  public boolean isValid() {
+    return this.valid;
+  }
+
+  /**
+   * Returns the index of this piece in the torrent.
+   */
+  public int getIndex() {
+    return this.index;
+  }
+
+  /**
+   * Returns the size, in bytes, of this piece.
+   *
+   * <p>
+   * All pieces, except the last one, are expected to have the same size.
+   * </p>
+   */
+  public long size() {
+    return this.length;
+  }
+
+  /**
+   * Tells whether this piece is available in the current connected peer swarm.
+   */
+  public boolean available() {
+    return this.seen > 0;
+  }
+
+  /**
+   * Mark this piece as being seen at the given peer.
+   *
+   * @param peer The sharing peer this piece has been seen available at.
+   */
+  public void seenAt(SharingPeer peer) {
+    this.seen++;
+  }
+
+  /**
+   * Mark this piece as no longer being available at the given peer.
+   *
+   * @param peer The sharing peer from which the piece is no longer available.
+   */
+  public void noLongerAt(SharingPeer peer) {
+    this.seen--;
+  }
+
+  void setValid(boolean valid) {
+    this.valid = valid;
+  }
+
+  /**
+   * Validates this piece.
+   *
+   * @return Returns true if this piece, as stored in the underlying byte
+   * storage, is valid, i.e. its SHA1 sum matches the one from the torrent
+   * meta-info.
+   */
+  public boolean validate(SharedTorrent torrent, Piece piece) throws IOException {
+
+    logger.trace("Validating {}...", this);
+
+    // TODO: remove cast to int when large ByteBuffer support is
+    // implemented in Java.
+    byte[] pieceBytes = data.array();
+    final byte[] calculatedHash = TorrentUtils.calculateSha1Hash(pieceBytes);
+    this.valid = Arrays.equals(calculatedHash, this.hash);
+    logger.trace("validating result of piece {} is {}", this.index, this.valid);
+
+    return this.isValid();
+  }
+
+  /**
+   * Internal piece data read function.
+   *
+   * <p>
+   * This function will read the piece data without checking if the piece has
+   * been validated. It is simply meant at factoring-in the common read code
+   * from the validate and read functions.
+   * </p>
+   *
+   * @param offset Offset inside this piece where to start reading.
+   * @param length Number of bytes to read from the piece.
+   * @return A byte buffer containing the piece data.
+   * @throws IllegalArgumentException If <em>offset + length</em> goes over
+   *                                  the piece boundary.
+   * @throws IOException              If the read can't be completed (I/O error, or EOF
+   *                                  reached, which can happen if the piece is not complete).
+   */
+  private ByteBuffer _read(long offset, long length, ByteBuffer buffer) throws IOException {
+    if (offset + length > this.length) {
+      throw new IllegalArgumentException("Piece#" + this.index +
+              " overrun (" + offset + " + " + length + " > " +
+              this.length + ") !");
+    }
+
+    // TODO: remove cast to int when large ByteBuffer support is
+    // implemented in Java.
+    int position = buffer.position();
+    byte[] bytes = this.pieceStorage.readPiecePart(this.index, (int)offset, (int)length);
+    buffer.put(bytes);
+    buffer.rewind();
+    buffer.limit(bytes.length + position);
+    return buffer;
+  }
+
+  /**
+   * Read a piece block from the underlying byte storage.
+   *
+   * <p>
+   * This is the public method for reading this piece's data, and it will
+   * only succeed if the piece is complete and valid on disk, thus ensuring
+   * any data that comes out of this function is valid piece data we can send
+   * to other peers.
+   * </p>
+   *
+   * @param offset Offset inside this piece where to start reading.
+   * @param length Number of bytes to read from the piece.
+   * @return A byte buffer containing the piece data.
+   * @throws IllegalArgumentException If <em>offset + length</em> goes over
+   *                                  the piece boundary.
+   * @throws IllegalStateException    If the piece is not valid when attempting
+   *                                  to read it.
+   * @throws IOException              If the read can't be completed (I/O error, or EOF
+   *                                  reached, which can happen if the piece is not complete).
+   */
+  public ByteBuffer read(long offset, int length, ByteBuffer block)
+          throws IllegalArgumentException, IllegalStateException, IOException {
+    if (!this.valid) {
+      throw new IllegalStateException("Attempting to read an " +
+              "known-to-be invalid piece!");
+    }
+
+    return this._read(offset, length, block);
+  }
+
+  /**
+   * Record the given block at the given offset in this piece.
+   *
+   * @param block  The ByteBuffer containing the block data.
+   * @param offset The block offset in this piece.
+   */
+  public void record(ByteBuffer block, int offset) {
+    if (this.data == null) {
+      // TODO: remove cast to int when large ByteBuffer support is
+      // implemented in Java.
+      this.data = ByteBuffer.allocate((int) this.length);
+    }
+
+    int pos = block.position();
+    this.data.position(offset);
+    this.data.put(block);
+    block.position(pos);
+  }
+
+  public void finish() throws IOException {
+    this.data.rewind();
+    logger.trace("Recording {}...", this);
+    try {
+      pieceStorage.savePiece(index, this.data.array());
+    } finally {
+      this.data = null;
+    }
+  }
+
+  /**
+   * Return a human-readable representation of this piece.
+   */
+  public String toString() {
+    return String.format("piece#%4d%s",
+            this.index,
+            this.isValid() ? "+" : "-");
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof Piece) {
+      return this.index == ((Piece) obj).index;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Piece comparison function for ordering pieces based on their
+   * availability.
+   *
+   * @param other The piece to compare with, should not be <em>null</em>.
+   */
+  public int compareTo(Piece other) {
+    // return true for the same pieces, otherwise sort by time seen, then by index;
+    if (this.equals(other)) {
+      return 0;
+    } else if (this.seen == other.seen) {
+      return new Integer(this.index).compareTo(other.index);
+    } else if (this.seen < other.seen) {
+      return -1;
+    } else {
+      return 1;
+    }
+  }
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PieceInformation.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PieceInformation.java
new file mode 100644
index 0000000..7cadaa3
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/PieceInformation.java
@@ -0,0 +1,15 @@
+package com.turn.ttorrent.client;
+
+public interface PieceInformation {
+
+  /**
+   * @return piece index. Indexing starts from zero
+   */
+  int getIndex();
+
+  /**
+   * @return piece size. This value must be equals piece size specified by metadata excluding last piece
+   */
+  int getSize();
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SelectorFactoryImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SelectorFactoryImpl.java
new file mode 100644
index 0000000..e97133e
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SelectorFactoryImpl.java
@@ -0,0 +1,30 @@
+/*
+ * 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.client;
+
+import com.turn.ttorrent.network.SelectorFactory;
+
+import java.io.IOException;
+import java.nio.channels.Selector;
+
+public class SelectorFactoryImpl implements SelectorFactory {
+
+  @Override
+  public Selector newSelector() throws IOException {
+    return Selector.open();
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SharedTorrent.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SharedTorrent.java
new file mode 100644
index 0000000..a1b6035
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SharedTorrent.java
@@ -0,0 +1,851 @@
+/**
+ * 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.client;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.client.peer.PeerActivityListener;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.client.storage.PieceStorage;
+import com.turn.ttorrent.client.storage.TorrentByteStorage;
+import com.turn.ttorrent.client.strategy.*;
+import com.turn.ttorrent.common.Optional;
+import com.turn.ttorrent.common.*;
+import org.apache.commons.io.FileUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Future;
+
+
+/**
+ * A torrent shared by the BitTorrent client.
+ * <p/>
+ * <p>
+ * The {@link SharedTorrent} class extends the Torrent class with all the data
+ * and logic required by the BitTorrent client implementation.
+ * </p>
+ * <p/>
+ * <p>
+ * <em>Note:</em> this implementation currently only supports single-file
+ * torrents.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+
+// 创建torrent对象
+// 接口继承的代码不用怎么看,反正实现是在文件里的
+public class SharedTorrent implements PeerActivityListener, TorrentMetadata, TorrentInfo {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(SharedTorrent.class);
+
+  private final static RequestStrategy DEFAULT_REQUEST_STRATEGY = new RequestStrategyImplAnyInteresting();
+
+  /**
+   * End-game trigger ratio.
+   *
+   * <p>
+   * Eng-game behavior (requesting already requested pieces from available
+   * and ready peers to try to speed-up the end of the transfer) will only be
+   * enabled when the ratio of completed pieces over total pieces in the
+   * torrent is over this value.
+   * </p>
+   */
+  private static final float ENG_GAME_COMPLETION_RATIO = 0.95f;
+  private static final int END_GAME_STATIC_PIECES_COUNT = 20;
+  private static final long END_GAME_INVOCATION_PERIOD_MS = 2000;
+
+  private final TorrentStatistic myTorrentStatistic;
+
+  private long myLastAnnounceTime = -1;
+  private int mySeedersCount = 0;
+
+  private final PieceStorage pieceStorage;
+  private boolean isFileChannelOpen = false;
+  private final Map<Integer, Future<?>> myValidationFutures;
+  private final TorrentMetadata myTorrentMetadata;
+  private final long myTorrentTotalSize;
+
+  private final int pieceLength;
+  private final ByteBuffer piecesHashes;
+
+  private boolean initialized;
+  private Piece[] pieces;
+  private final BitSet completedPieces;
+  private final BitSet requestedPieces;
+  private final RequestStrategy myRequestStrategy;
+  private final EventDispatcher eventDispatcher;
+
+  private final List<SharingPeer> myDownloaders = new CopyOnWriteArrayList<SharingPeer>();
+  private final EndGameStrategy endGameStrategy = new EndGameStrategyImpl(2);
+  private volatile long endGameEnabledOn = -1;
+
+  private volatile ClientState clientState = ClientState.WAITING;
+  private static final int MAX_VALIDATION_TASK_COUNT = 200;
+  private static final int MAX_REQUESTED_PIECES_PER_TORRENT = 100;
+
+  /**
+   * Create a new shared torrent from meta-info
+   *
+   * @param torrentMetadata The meta-info
+   * @param eventDispatcher
+   */
+  public SharedTorrent(TorrentMetadata torrentMetadata, PieceStorage pieceStorage, RequestStrategy requestStrategy,
+                       TorrentStatistic torrentStatistic, EventDispatcher eventDispatcher) {
+    myTorrentMetadata = torrentMetadata;
+    this.pieceStorage = pieceStorage;
+    this.eventDispatcher = eventDispatcher;
+    myTorrentStatistic = torrentStatistic;
+    myValidationFutures = new HashMap<Integer, Future<?>>();
+    long totalSize = 0;
+    for (TorrentFile torrentFile : myTorrentMetadata.getFiles()) {
+      totalSize += torrentFile.size;
+    }
+    myTorrentTotalSize = totalSize;
+    this.myRequestStrategy = requestStrategy;
+
+    this.pieceLength = myTorrentMetadata.getPieceLength();
+    this.piecesHashes = ByteBuffer.wrap(myTorrentMetadata.getPiecesHashes());
+
+    if (this.piecesHashes.capacity() / Constants.PIECE_HASH_SIZE *
+            (long) this.pieceLength < myTorrentTotalSize) {
+      throw new IllegalArgumentException("Torrent size does not " +
+              "match the number of pieces and the piece size!");
+    }
+
+    this.initialized = false;
+    this.pieces = new Piece[0];
+    this.completedPieces = new BitSet(torrentMetadata.getPiecesCount());
+    this.requestedPieces = new BitSet();
+  }
+
+  public static SharedTorrent fromFile(File source, PieceStorage pieceStorage, TorrentStatistic torrentStatistic)
+          throws IOException {
+    byte[] data = FileUtils.readFileToByteArray(source);
+    TorrentMetadata torrentMetadata = new TorrentParser().parse(data);
+    return new SharedTorrent(torrentMetadata, pieceStorage, DEFAULT_REQUEST_STRATEGY, torrentStatistic, new EventDispatcher());
+  }
+
+  private synchronized void closeFileChannelIfNecessary() throws IOException {
+    if (isFileChannelOpen && myDownloaders.size() == 0) {
+      logger.debug("Closing file  channel for {} if necessary. Downloaders: {}", getHexInfoHash(), myDownloaders.size());
+      this.pieceStorage.close();
+      isFileChannelOpen = false;
+    }
+  }
+
+  /**
+   * Get the number of bytes uploaded for this torrent.
+   */
+  public long getUploaded() {
+    return myTorrentStatistic.getUploadedBytes();
+  }
+
+  /**
+   * Get the number of bytes downloaded for this torrent.
+   * <p/>
+   * <p>
+   * <b>Note:</b> this could be more than the torrent's length, and should
+   * not be used to determine a completion percentage.
+   * </p>
+   */
+  public long getDownloaded() {
+    return myTorrentStatistic.getDownloadedBytes();
+  }
+
+  /**
+   * Get the number of bytes left to download for this torrent.
+   */
+  public long getLeft() {
+    return myTorrentStatistic.getLeftBytes();
+  }
+
+  public int getSeedersCount() {
+    return mySeedersCount;
+  }
+
+  public void setSeedersCount(int seedersCount) {
+    mySeedersCount = seedersCount;
+  }
+
+  public long getLastAnnounceTime() {
+    return myLastAnnounceTime;
+  }
+
+  public void setLastAnnounceTime(long lastAnnounceTime) {
+    myLastAnnounceTime = lastAnnounceTime;
+  }
+
+  /**
+   * Tells whether this torrent has been fully initialized yet.
+   */
+  public boolean isInitialized() {
+    return this.initialized;
+  }
+
+  /**
+   * Stop the torrent initialization as soon as possible.
+   */
+  public void stop() {
+  }
+
+  /**
+   * Build this torrent's pieces array.
+   * <p/>
+   * <p>
+   * Hash and verify any potentially present local data and create this
+   * torrent's pieces array from their respective hash provided in the
+   * torrent meta-info.
+   * </p>
+   * <p/>
+   * <p>
+   * This function should be called soon after the constructor to initialize
+   * the pieces array.
+   * </p>
+   */
+  public synchronized void init() throws InterruptedException, IOException {
+    setClientState(ClientState.VALIDATING);
+
+    if (this.isInitialized()) {
+      throw new IllegalStateException("Torrent was already initialized!");
+    }
+
+    hashSingleThread();
+
+    this.initialized = true;
+  }
+
+  private void initPieces() {
+    int nPieces = (int) (Math.ceil(
+            (double) myTorrentTotalSize / this.pieceLength));
+    this.pieces = new Piece[nPieces];
+    this.piecesHashes.clear();
+  }
+
+  private void hashSingleThread() {
+    initPieces();
+
+    logger.debug("Analyzing local data for {} with {} threads...",
+            myTorrentMetadata.getDirectoryName(), TorrentCreator.HASHING_THREADS_COUNT);
+    for (int idx = 0; idx < this.pieces.length; idx++) {
+      byte[] hash = new byte[Constants.PIECE_HASH_SIZE];
+      this.piecesHashes.get(hash);
+
+      // The last piece may be shorter than the torrent's global piece
+      // length. Let's make sure we get the right piece length in any
+      // situation.
+      long off = ((long) idx) * this.pieceLength;
+      long len = Math.min(
+              myTorrentTotalSize - off,
+              this.pieceLength);
+
+      Piece piece = new Piece(this.pieceStorage, idx, len, hash
+      );
+      this.pieces[idx] = piece;
+      piece.setValid(pieceStorage.getAvailablePieces().get(idx));
+
+      if (piece.isValid()) {
+        this.completedPieces.set(piece.getIndex());
+      }
+    }
+  }
+
+  public synchronized void close() {
+    logger.trace("Closing torrent", myTorrentMetadata.getDirectoryName());
+    try {
+      this.pieceStorage.close();
+      isFileChannelOpen = false;
+    } catch (IOException ioe) {
+      logger.error("Error closing torrent byte storage: {}",
+              ioe.getMessage());
+    }
+  }
+
+  public synchronized void closeFully() {
+    logger.trace("Closing torrent", myTorrentMetadata.getDirectoryName());
+    try {
+      this.pieceStorage.closeFully();
+      isFileChannelOpen = false;
+    } catch (IOException ioe) {
+      logger.error("Error closing torrent byte storage: {}",
+              ioe.getMessage());
+    }
+  }
+
+  /**
+   * Retrieve a piece object by index.
+   *
+   * @param index The index of the piece in this torrent.
+   */
+  public Piece getPiece(int index) {
+    if (this.pieces == null) {
+      throw new IllegalStateException("Torrent not initialized yet.");
+    }
+
+    if (index >= this.pieces.length) {
+      throw new IllegalArgumentException("Invalid piece index!");
+    }
+
+    return this.pieces[index];
+  }
+
+  /**
+   * Return a copy of the bit field of available pieces for this torrent.
+   * <p/>
+   * <p>
+   * Available pieces are pieces available in the swarm, and it does not
+   * include our own pieces.
+   * </p>
+   */
+  public BitSet getAvailablePieces() {
+    if (!this.isInitialized()) {
+      throw new IllegalStateException("Torrent not yet initialized!");
+    }
+
+    BitSet availablePieces = new BitSet(this.pieces.length);
+
+    synchronized (this.pieces) {
+      for (Piece piece : this.pieces) {
+        if (piece.available()) {
+          availablePieces.set(piece.getIndex());
+        }
+      }
+    }
+
+    return availablePieces;
+  }
+
+  /**
+   * Return a copy of the completed pieces bitset.
+   */
+  public BitSet getCompletedPieces() {
+    if (!this.isInitialized()) {
+      throw new IllegalStateException("Torrent not yet initialized!");
+    }
+
+    return pieceStorage.getAvailablePieces();
+  }
+
+  /**
+   * Tells whether this torrent has been fully downloaded, or is fully
+   * available locally.
+   */
+  public synchronized boolean isComplete() {
+    return this.pieces.length > 0
+            && pieceStorage.getAvailablePieces().cardinality() == myTorrentMetadata.getPiecesCount();
+  }
+
+  /**
+   * Finalize the download of this torrent.
+   * <p/>
+   * <p>
+   * This realizes the final, pre-seeding phase actions on this torrent,
+   * which usually consists in putting the torrent data in their final form
+   * and at their target location.
+   * </p>
+   *
+   * @see TorrentByteStorage#finish
+   */
+  public synchronized void finish() {
+    if (!this.isInitialized()) {
+      throw new IllegalStateException("Torrent not yet initialized!");
+    }
+
+    if (!this.isComplete()) {
+      throw new IllegalStateException("Torrent download is not complete!");
+    }
+
+    eventDispatcher.multicaster().downloadComplete();
+    setClientState(ClientState.SEEDING);
+  }
+
+  public boolean isFinished() {
+    return pieceStorage.getAvailablePieces().cardinality() == myTorrentMetadata.getPiecesCount();
+  }
+
+  public ClientState getClientState() {
+    return this.clientState;
+  }
+
+  public void setClientState(ClientState clientState) {
+    this.clientState = clientState;
+  }
+
+  /**
+   * Mark a piece as completed, decrementing the piece size in bytes from our
+   * left bytes to download counter.
+   */
+  public synchronized void markCompleted(Piece piece) {
+    if (this.completedPieces.get(piece.getIndex())) {
+      return;
+    }
+
+    // A completed piece means that's that much data left to download for
+    // this torrent.
+    myTorrentStatistic.addLeft(-piece.size());
+    this.completedPieces.set(piece.getIndex());
+    if (completedPieces.cardinality() == getPiecesCount()) {
+      logger.info("all pieces are received for torrent {}. Validating...", this);
+    }
+  }
+
+  public synchronized void markUncompleted(Piece piece) {
+    if (!this.completedPieces.get(piece.getIndex())) {
+      return;
+    }
+
+    removeValidationFuture(piece);
+    myTorrentStatistic.addLeft(piece.size());
+    this.completedPieces.clear(piece.getIndex());
+  }
+
+  public synchronized void removeValidationFuture(Piece piece) {
+    myValidationFutures.remove(piece.getIndex());
+  }
+
+  public void notifyPieceDownloaded(Piece piece, SharingPeer peer) {
+    eventDispatcher.multicaster().pieceDownloaded(piece, peer);
+  }
+
+  /** PeerActivityListener handler(s). *************************************/
+
+  /**
+   * Peer choked handler.
+   * <p/>
+   * <p>
+   * When a peer chokes, the requests made to it are canceled and we need to
+   * mark the eventually piece we requested from it as available again for
+   * download tentative from another peer.
+   * </p>
+   *
+   * @param peer The peer that choked.
+   */
+  @Override
+  public synchronized void handlePeerChoked(SharingPeer peer) {
+    Set<Piece> pieces = peer.getRequestedPieces();
+
+    if (pieces.size() > 0) {
+      for (Piece piece : pieces) {
+        this.requestedPieces.set(piece.getIndex(), false);
+      }
+    }
+
+    logger.trace("Peer {} choked, we now have {} outstanding " +
+                    "request(s): {}.",
+            new Object[]{
+                    peer,
+                    this.requestedPieces.cardinality(),
+                    this.requestedPieces
+            });
+  }
+
+  /**
+   * Peer ready handler.
+   * <p/>
+   * <p>
+   * When a peer becomes ready to accept piece block requests, select a piece
+   * to download and go for it.
+   * </p>
+   *
+   * @param peer The peer that became ready.
+   */
+  @Override
+  public void handlePeerReady(SharingPeer peer) {
+    initIfNecessary(peer);
+
+    RequestsCollection requestsCollection = getRequestsCollection(peer);
+    requestsCollection.sendAllRequests();
+  }
+
+  @NotNull
+  private synchronized RequestsCollection getRequestsCollection(final SharingPeer peer) {
+    if (myValidationFutures.size() > MAX_VALIDATION_TASK_COUNT) return RequestsCollection.Empty.INSTANCE;
+
+    if (this.requestedPieces.cardinality() > MAX_REQUESTED_PIECES_PER_TORRENT) return RequestsCollection.Empty.INSTANCE;
+
+    int completedAndValidated = pieceStorage.getAvailablePieces().cardinality();
+
+    boolean turnOnEndGame = completedAndValidated > getPiecesCount() * ENG_GAME_COMPLETION_RATIO ||
+            completedAndValidated > getPiecesCount() - END_GAME_STATIC_PIECES_COUNT;
+    if (turnOnEndGame) {
+      long now = System.currentTimeMillis();
+      if (now - END_GAME_INVOCATION_PERIOD_MS > endGameEnabledOn) {
+        endGameEnabledOn = now;
+        logger.info("Running end-game mode, currently available {}/{} pieces",
+                pieceStorage.getAvailablePieces().cardinality(),
+                getPieceCount());
+        return endGameStrategy.collectRequests(pieces, myDownloaders);
+      }
+      return RequestsCollection.Empty.INSTANCE;
+    }
+
+    final BitSet interesting = peer.getAvailablePieces();
+    interesting.andNot(this.completedPieces);
+    interesting.andNot(this.requestedPieces);
+
+    int maxRequestingPieces = Math.min(10, interesting.cardinality());
+    int currentlyDownloading = peer.getDownloadingPiecesCount();
+    Map<Piece, List<SharingPeer>> toRequest = new HashMap<Piece, List<SharingPeer>>();
+    while (currentlyDownloading < maxRequestingPieces) {
+      if (!peer.isConnected()) {
+        break;
+      }
+
+      if (interesting.cardinality() == 0) {
+        return RequestsCollection.Empty.INSTANCE;
+      }
+
+      Piece chosen = myRequestStrategy.choosePiece(interesting, pieces);
+      if (chosen == null) {
+        logger.info("chosen piece is null");
+        break;
+      }
+      this.requestedPieces.set(chosen.getIndex());
+      currentlyDownloading++;
+      toRequest.put(chosen, Collections.singletonList(peer));
+      interesting.clear(chosen.getIndex());
+    }
+
+    return new RequestsCollectionImpl(toRequest);
+  }
+
+  public synchronized void initIfNecessary(SharingPeer peer) {
+    if (!isInitialized()) {
+      try {
+        init();
+      } catch (InterruptedException e) {
+        logger.info("Interrupted init", e);
+        peer.unbind(true);
+        return;
+      } catch (IOException e) {
+        logger.info("IOE during init", e);
+        peer.unbind(true);
+        return;
+      }
+    }
+  }
+
+  /**
+   * Piece availability handler.
+   * <p/>
+   * <p>
+   * Handle updates in piece availability from a peer's HAVE message. When
+   * this happens, we need to mark that piece as available from the peer.
+   * </p>
+   *
+   * @param peer  The peer we got the update from.
+   * @param piece The piece that became available.
+   */
+  @Override
+  public void handlePieceAvailability(SharingPeer peer, Piece piece) {
+    boolean isPeerInteresting = !this.completedPieces.get(piece.getIndex()) &&
+            !this.requestedPieces.get(piece.getIndex());
+    if (isPeerInteresting) {
+      peer.interesting();
+    }
+
+    piece.seenAt(peer);
+
+    logger.trace("Peer {} contributes {} piece(s) [{}/{}/{}].",
+            new Object[]{
+                    peer,
+                    peer.getAvailablePieces().cardinality(),
+                    this.completedPieces.cardinality(),
+                    this.getAvailablePieces().cardinality(),
+                    this.pieces.length
+            });
+
+    if (!peer.isChoked() &&
+            peer.isInteresting() &&
+            !peer.isDownloading()) {
+      this.handlePeerReady(peer);
+    }
+  }
+
+  /**
+   * Bit field availability handler.
+   * <p/>
+   * <p>
+   * Handle updates in piece availability from a peer's BITFIELD message.
+   * When this happens, we need to mark in all the pieces the peer has that
+   * they can be reached through this peer, thus augmenting the global
+   * availability of pieces.
+   * </p>
+   *
+   * @param peer            The peer we got the update from.
+   * @param availablePieces The pieces availability bit field of the peer.
+   */
+  @Override
+  public void handleBitfieldAvailability(SharingPeer peer,
+                                         BitSet availablePieces) {
+    // Determine if the peer is interesting for us or not, and notify it.
+    BitSet interesting = (BitSet) availablePieces.clone();
+    synchronized (this) {
+      interesting.andNot(this.completedPieces);
+      interesting.andNot(this.requestedPieces);
+    }
+    // Record the peer has all the pieces it told us it had.
+    for (int i = availablePieces.nextSetBit(0); i >= 0;
+         i = availablePieces.nextSetBit(i + 1)) {
+      this.pieces[i].seenAt(peer);
+    }
+
+    if (interesting.cardinality() == 0) {
+      peer.notInteresting();
+    } else {
+      peer.interesting();
+    }
+
+    logger.debug("Peer {} contributes {} piece(s), total pieces count: {}.",
+            new Object[]{
+                    peer,
+                    availablePieces.cardinality(),
+                    myTorrentMetadata.getPiecesCount()
+            });
+  }
+
+  public int getDownloadersCount() {
+    return myDownloaders.size();
+  }
+
+  @Override
+  public void afterPeerRemoved(SharingPeer peer) {
+
+  }
+
+  /**
+   * Piece upload completion handler.
+   * <p/>
+   * <p>
+   * When a piece has been sent to a peer, we just record that we sent that
+   * many bytes. If the piece is valid on the peer's side, it will send us a
+   * HAVE message and we'll record that the piece is available on the peer at
+   * that moment (see <code>handlePieceAvailability()</code>).
+   * </p>
+   *
+   * @param peer  The peer we got this piece from.
+   * @param piece The piece in question.
+   */
+  @Override
+  public void handlePieceSent(SharingPeer peer, Piece piece) {
+    logger.trace("Completed upload of {} to {}.", piece, peer);
+    myTorrentStatistic.addUploaded(piece.size());
+  }
+
+  /**
+   * Piece download completion handler.
+   * <p/>
+   * <p>
+   * If the complete piece downloaded is valid, we can record in the torrent
+   * completedPieces bit field that we know have this piece.
+   * </p>
+   *
+   * @param peer  The peer we got this piece from.
+   * @param piece The piece in question.
+   */
+  @Override
+  public void handlePieceCompleted(SharingPeer peer,
+                                   Piece piece) throws IOException {
+    // Regardless of validity, record the number of bytes downloaded and
+    // mark the piece as not requested anymore
+    myTorrentStatistic.addDownloaded(piece.size());
+    this.requestedPieces.set(piece.getIndex(), false);
+
+    logger.trace("We now have {} piece(s) and {} outstanding request(s): {}",
+            new Object[]{
+                    this.completedPieces.cardinality(),
+                    this.requestedPieces.cardinality(),
+                    this.requestedPieces
+            });
+  }
+
+  /**
+   * Peer disconnection handler.
+   * <p/>
+   * <p>
+   * When a peer disconnects, we need to mark in all of the pieces it had
+   * available that they can't be reached through this peer anymore.
+   * </p>
+   *
+   * @param peer The peer we got this piece from.
+   */
+  @Override
+  public synchronized void handlePeerDisconnected(SharingPeer peer) {
+    BitSet availablePieces = peer.getAvailablePieces();
+
+    for (int i = availablePieces.nextSetBit(0); i >= 0;
+         i = availablePieces.nextSetBit(i + 1)) {
+      this.pieces[i].noLongerAt(peer);
+    }
+
+    Set<Piece> requested = peer.getRequestedPieces();
+    if (requested != null) {
+      for (Piece piece : requested) {
+        this.requestedPieces.set(piece.getIndex(), false);
+      }
+    }
+
+    myDownloaders.remove(peer);
+
+    try {
+      closeFileChannelIfNecessary();
+    } catch (IOException e) {
+      logger.info("I/O error on attempt to close file storage: " + e.toString());
+    }
+
+    logger.debug("Peer {} went away with {} piece(s) [{}/{}].",
+            new Object[]{
+                    peer,
+                    availablePieces.cardinality(),
+                    this.completedPieces.cardinality(),
+                    this.pieces.length
+            });
+    logger.trace("We now have {} piece(s) and {} outstanding request(s): {}",
+            new Object[]{
+                    this.completedPieces.cardinality(),
+                    this.requestedPieces.cardinality(),
+                    this.requestedPieces
+            });
+    eventDispatcher.multicaster().peerDisconnected(peer);
+  }
+
+  @Override
+  public synchronized void handleIOException(SharingPeer peer,
+                                             IOException ioe) {
+    eventDispatcher.multicaster().downloadFailed(ioe);
+  }
+
+  @Override
+  public synchronized void handleNewPeerConnected(SharingPeer peer) {
+    initIfNecessary(peer);
+    eventDispatcher.multicaster().peerConnected(peer);
+  }
+
+  @Override
+  public String toString() {
+    return "SharedTorrent{" +
+            Arrays.toString(TorrentUtils.getTorrentFileNames(myTorrentMetadata).toArray()) +
+            "}";
+  }
+
+  @Override
+  public String getDirectoryName() {
+    return myTorrentMetadata.getDirectoryName();
+  }
+
+  @Override
+  public List<TorrentFile> getFiles() {
+    return myTorrentMetadata.getFiles();
+  }
+
+  @Nullable
+  @Override
+  public List<List<String>> getAnnounceList() {
+    return myTorrentMetadata.getAnnounceList();
+  }
+
+  @Nullable
+  @Override
+  public String getAnnounce() {
+    return myTorrentMetadata.getAnnounce();
+  }
+
+  @Override
+  public Optional<Long> getCreationDate() {
+    return myTorrentMetadata.getCreationDate();
+  }
+
+  @Override
+  public Optional<String> getComment() {
+    return myTorrentMetadata.getComment();
+  }
+
+  @Override
+  public Optional<String> getCreatedBy() {
+    return myTorrentMetadata.getCreatedBy();
+  }
+
+  @Override
+  public int getPieceLength() {
+    return myTorrentMetadata.getPieceLength();
+  }
+
+  @Override
+  public byte[] getPiecesHashes() {
+    return myTorrentMetadata.getPiecesHashes();
+  }
+
+  @Override
+  public boolean isPrivate() {
+    return myTorrentMetadata.isPrivate();
+  }
+
+  @Override
+  public int getPiecesCount() {
+    return myTorrentMetadata.getPiecesCount();
+  }
+
+  @Override
+  public byte[] getInfoHash() {
+    return myTorrentMetadata.getInfoHash();
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return myTorrentMetadata.getHexInfoHash();
+  }
+
+  @Override
+  public int getPieceCount() {
+    return getPiecesCount();
+  }
+
+  @Override
+  public long getPieceSize(int pieceIdx) {
+    return getPieceLength();
+  }
+
+  public synchronized void savePieceAndValidate(Piece p) throws IOException {
+//    p.finish();
+  }
+
+  public synchronized void markCompletedAndAddValidationFuture(Piece piece, Future<?> validationFuture) {
+    this.markCompleted(piece);
+    myValidationFutures.put(piece.getIndex(), validationFuture);
+  }
+
+  public synchronized boolean isAllPiecesOfPeerCompletedAndValidated(SharingPeer peer) {
+    final BitSet availablePieces = peer.getAvailablePieces();
+    for (Piece piece : pieces) {
+      final boolean peerHaveCurrentPiece = availablePieces.get(piece.getIndex());
+      if (!peerHaveCurrentPiece) continue;
+      if (!completedPieces.get(piece.getIndex())) return false;
+      if (myValidationFutures.get(piece.getIndex()) != null) return false;
+    }
+    return true;
+  }
+
+  public void addConnectedPeer(SharingPeer sharingPeer) {
+    myDownloaders.add(sharingPeer);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SharingPeerFactory.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SharingPeerFactory.java
new file mode 100644
index 0000000..f5263e1
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SharingPeerFactory.java
@@ -0,0 +1,18 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.client.peer.SharingPeer;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+
+public interface SharingPeerFactory {
+
+  SharingPeer createSharingPeer(String host,
+                                int port,
+                                ByteBuffer peerId,
+                                SharedTorrent torrent,
+                                ByteChannel channel,
+                                String clientIdentifier,
+                                int clientVersion);
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SimpleClient.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SimpleClient.java
new file mode 100644
index 0000000..cc43ccd
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/SimpleClient.java
@@ -0,0 +1,122 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentStatistic;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+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.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// 客户端
+public class SimpleClient {
+
+  private final static int DEFAULT_EXECUTOR_SIZE = 10;
+  private final CommunicationManager communicationManager;
+
+  public SimpleClient() {
+    this(DEFAULT_EXECUTOR_SIZE, DEFAULT_EXECUTOR_SIZE);// 构造函数重载,载入下面的传入两个函数的函数
+  }
+
+  public SimpleClient(int workingExecutorSize, int validatorExecutorSize) {
+    communicationManager = new CommunicationManager(Executors.newFixedThreadPool(workingExecutorSize), Executors.newFixedThreadPool(validatorExecutorSize));
+  }
+
+  public void stop() {
+    stop(60, TimeUnit.SECONDS);
+  }
+
+  public void stop(int timeout, TimeUnit timeUnit) {
+    communicationManager.stop(timeout, timeUnit);
+    Exception interruptedException = null;
+    boolean anyFailedByTimeout = false;
+    for (ExecutorService executorService : Arrays.asList(
+            communicationManager.getExecutor(),
+            communicationManager.getPieceValidatorExecutor())) {
+      executorService.shutdown();
+
+      //if the thread is already interrupted don't try to await termination
+      if (Thread.currentThread().isInterrupted()) continue;
+
+      try {
+        if (!executorService.awaitTermination(timeout, timeUnit)) {
+          anyFailedByTimeout = true;
+        }
+      } catch (InterruptedException e) {
+        interruptedException = e;
+      }
+    }
+    if (interruptedException != null) {
+      throw new RuntimeException("Thread was interrupted, " +
+              "shutdown methods are invoked but maybe tasks are not finished yet", interruptedException);
+    }
+    if (anyFailedByTimeout)
+      throw new RuntimeException("At least one executor was not fully shutdown because timeout was elapsed");
+
+  }
+
+  //torrentFile 是种子路径
+  public void downloadTorrent(String torrentFile, String downloadDir, InetAddress iPv4Address) throws IOException, InterruptedException {
+    communicationManager.start(iPv4Address);
+    final Semaphore semaphore = new Semaphore(0);
+    List<TorrentListener> listeners = Collections.<TorrentListener>singletonList(
+            new TorrentListenerWrapper() {
+
+              @Override
+              public void validationComplete(int validpieces, int totalpieces) {
+                if (validpieces == totalpieces) semaphore.release();
+              }
+
+              @Override
+              public void downloadComplete() {
+                semaphore.release();
+              }
+            }
+    );
+    TorrentManager torrentManager = communicationManager.addTorrent(torrentFile, downloadDir, listeners);
+    semaphore.acquire();
+  }
+
+  private TorrentManager startDownloading(String torrentFile, String downloadDir, InetAddress iPv4Address) throws IOException {
+    communicationManager.start(iPv4Address);
+    return communicationManager.addTorrent(torrentFile, downloadDir);
+  }
+
+  public TorrentManager downloadTorrentAsync(String torrentFile,
+                                             String downloadDir,
+                                             InetAddress iPv4Address) throws IOException {
+    return startDownloading(torrentFile, downloadDir, iPv4Address);
+  }
+
+
+  /**
+   * Get statistics for a given torrent file
+   * @param dotTorrentFilePath
+   * @return
+   * @throws IOException If unable to get torrent metadata
+   * @throws IllegalStateException If the torrent has not been loaded
+   */
+  public TorrentStatistic getStatistics(String dotTorrentFilePath) throws IOException {
+    FileMetadataProvider metadataProvider = new FileMetadataProvider(dotTorrentFilePath);
+    TorrentMetadata metadata = metadataProvider.getTorrentMetadata();
+    LoadedTorrent loadedTorrent = communicationManager.getTorrentsStorage().getLoadedTorrent(metadata.getHexInfoHash());
+    if (loadedTorrent != null) {
+      return new TorrentStatistic(loadedTorrent.getTorrentStatistic());
+    }
+
+    throw new IllegalStateException("Torrent has not been loaded yet");
+
+  }
+
+
+  public boolean hasStop() {
+    return communicationManager.hasStop();
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentConnectionListener.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentConnectionListener.java
new file mode 100644
index 0000000..2c346ce
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentConnectionListener.java
@@ -0,0 +1,17 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.common.TorrentHash;
+
+import java.nio.channels.SocketChannel;
+
+/**
+ * @author Sergey.Pak
+ * Date: 9/9/13
+ * Time: 7:46 PM
+ */
+public interface TorrentConnectionListener {
+
+  boolean hasTorrent(TorrentHash torrentHash);
+
+  void handleNewPeerConnection(SocketChannel s, byte[] peerId, String hexInfoHash);
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentListener.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentListener.java
new file mode 100644
index 0000000..dcab6dc
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentListener.java
@@ -0,0 +1,57 @@
+package com.turn.ttorrent.client;
+
+public interface TorrentListener {
+
+  /**
+   * Invoked when connection with peer is established
+   *
+   * @param peerInformation specified information about peer
+   */
+  void peerConnected(PeerInformation peerInformation);
+
+  /**
+   * Invoked when connection with peer is closed.
+   *
+   * @param peerInformation specified information about peer
+   */
+  void peerDisconnected(PeerInformation peerInformation);
+
+  /**
+   * Invoked when piece is downloaded and validated
+   *
+   * @param pieceInformation specified information about piece
+   * @param peerInformation  specified information about peer
+   */
+  void pieceDownloaded(PieceInformation pieceInformation, PeerInformation peerInformation);
+
+  /**
+   * Invoked when downloading is fully downloaded (last piece is received and validated)
+   */
+  void downloadComplete();
+
+  /**
+   * invoked when piece is downloaded but not validated yet
+   *
+   * @param pieceInformation specified information about piece
+   * @param peerInformation  specified information about peer
+   */
+  void pieceReceived(PieceInformation pieceInformation, PeerInformation peerInformation);
+
+  /**
+   * Invoked when download was failed with any exception (e.g. some runtime exception or i/o exception in file operation).
+   *
+   * @param cause specified exception
+   */
+  void downloadFailed(Throwable cause);
+
+  /**
+   * Invoked when validation of torrent is done.
+   * If total pieces count and valid pieces count are equals it means that torrent is fully downloaded.
+   * {@link #downloadComplete()} listener will not be invoked in this case
+   *
+   * @param validpieces count of valid pieces. Must be not greater as #totalpieces
+   * @param totalpieces total pieces count in torrent
+   */
+  void validationComplete(int validpieces, int totalpieces);
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentListenerWrapper.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentListenerWrapper.java
new file mode 100644
index 0000000..57356fb
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentListenerWrapper.java
@@ -0,0 +1,39 @@
+package com.turn.ttorrent.client;
+
+public class TorrentListenerWrapper implements TorrentListener {
+
+  @Override
+  public void peerConnected(PeerInformation peerInformation) {
+
+  }
+
+  @Override
+  public void peerDisconnected(PeerInformation peerInformation) {
+
+  }
+
+  @Override
+  public void pieceDownloaded(PieceInformation pieceInformation, PeerInformation peerInformation) {
+
+  }
+
+  @Override
+  public void downloadComplete() {
+
+  }
+
+  @Override
+  public void downloadFailed(Throwable cause) {
+
+  }
+
+  @Override
+  public void pieceReceived(PieceInformation pieceInformation, PeerInformation peerInformation) {
+
+  }
+
+  @Override
+  public void validationComplete(int validpieces, int totalpieces) {
+
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentLoader.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentLoader.java
new file mode 100644
index 0000000..4b15f7d
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentLoader.java
@@ -0,0 +1,19 @@
+package com.turn.ttorrent.client;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+public interface TorrentLoader {
+
+  /**
+   * Creates or finds shared torrent instance for specified announceable torrent and return it
+   *
+   * @param loadedTorrent specified torrent
+   * @return shared torrent instance associated with current announceable torrent
+   * @throws IOException              if any io error occurs
+   */
+  @NotNull
+  SharedTorrent loadTorrent(@NotNull LoadedTorrent loadedTorrent) throws IOException;
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentLoaderImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentLoaderImpl.java
new file mode 100644
index 0000000..15bfbe3
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentLoaderImpl.java
@@ -0,0 +1,47 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.client.strategy.RequestStrategyImplAnyInteresting;
+import com.turn.ttorrent.common.TorrentMetadata;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+//实现加载SharedTorrent根据给定的 LoadedTorrent 对象,加载一个 SharedTorrent 对象
+public class TorrentLoaderImpl implements TorrentLoader {
+
+  @NotNull
+  private final TorrentsStorage myTorrentsStorage;
+
+  public TorrentLoaderImpl(@NotNull TorrentsStorage torrentsStorage) {
+    myTorrentsStorage = torrentsStorage;
+  }
+
+  @Override
+  @NotNull
+  public SharedTorrent loadTorrent(@NotNull LoadedTorrent loadedTorrent) throws IOException {
+
+    final String hexInfoHash = loadedTorrent.getTorrentHash().getHexInfoHash();
+    SharedTorrent old = myTorrentsStorage.getTorrent(hexInfoHash);
+    if (old != null) {
+      return old;
+    }
+
+    TorrentMetadata torrentMetadata;
+    try {
+      torrentMetadata = loadedTorrent.getMetadata();
+    } catch (IllegalStateException e) {
+      myTorrentsStorage.remove(hexInfoHash);
+      throw e;
+    }
+
+    final SharedTorrent sharedTorrent = new SharedTorrent(torrentMetadata, loadedTorrent.getPieceStorage(),
+            new RequestStrategyImplAnyInteresting(),
+            loadedTorrent.getTorrentStatistic(), loadedTorrent.getEventDispatcher());
+
+    old = myTorrentsStorage.putIfAbsentActiveTorrent(hexInfoHash, sharedTorrent);
+    if (old != null) {
+      return old;
+    }
+    return sharedTorrent;
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentManager.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentManager.java
new file mode 100644
index 0000000..08c4913
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentManager.java
@@ -0,0 +1,35 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.common.TorrentHash;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public interface TorrentManager extends TorrentHash {
+
+  /**
+   * add specified listener which will be notified on new events
+   *
+   * @param listener specified listener
+   */
+  void addListener(TorrentListener listener);
+
+  /**
+   * remove specified listener which was added earlier by {@link TorrentManager#addListener} method.
+   * You can receive events in this listener after execution of the method if notify method was invoked before this method
+   *
+   * @param listener specified listener
+   * @return true if listeners was removed otherwise false (e.g. listener was not found)
+   */
+  boolean removeListener(TorrentListener listener);
+
+  /**
+   * wait until download will be finished
+   *
+   * @param timeout  the maximum time to wait
+   * @param timeUnit the time unit of the timeout argument
+   * @throws InterruptedException if this thread was interrupted
+   * @throws TimeoutException     if timeout was elapsed
+   */
+  void awaitDownloadComplete(int timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException;
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentManagerImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentManagerImpl.java
new file mode 100644
index 0000000..de9cf57
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentManagerImpl.java
@@ -0,0 +1,58 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.common.TorrentHash;
+
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+class TorrentManagerImpl implements TorrentManager {
+
+  private final EventDispatcher eventDispatcher;
+  private final TorrentHash hash;
+
+  TorrentManagerImpl(EventDispatcher eventDispatcher, TorrentHash hash) {
+    this.eventDispatcher = eventDispatcher;
+    this.hash = hash;
+  }
+
+  @Override
+  public void addListener(TorrentListener listener) {
+    eventDispatcher.addListener(listener);
+  }
+
+  @Override
+  public boolean removeListener(TorrentListener listener) {
+    return eventDispatcher.removeListener(listener);
+  }
+
+  @Override
+  public byte[] getInfoHash() {
+    return hash.getInfoHash();
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return hash.getHexInfoHash();
+  }
+
+  @Override
+  public void awaitDownloadComplete(int timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException {
+    final Semaphore semaphore = new Semaphore(0);
+    TorrentListenerWrapper listener = new TorrentListenerWrapper() {
+      @Override
+      public void downloadComplete() {
+        semaphore.release();
+      }
+    };
+    try {
+      addListener(listener);
+      if (!semaphore.tryAcquire(timeout, timeUnit)) {
+        throw new TimeoutException("Unable to download torrent in specified timeout");
+      }
+    } finally {
+      removeListener(listener);
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentMetadataProvider.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentMetadataProvider.java
new file mode 100644
index 0000000..a389ec0
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentMetadataProvider.java
@@ -0,0 +1,21 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import com.turn.ttorrent.common.TorrentMetadata;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+public interface TorrentMetadataProvider {
+
+  /**
+   * load and return new {@link TorrentMetadata} instance from any source
+   *
+   * @return new torrent metadata instance
+   * @throws IOException               if any IO error occurs
+   * @throws InvalidBEncodingException if specified source has invalid BEP format or missed required fields
+   */
+  @NotNull
+  TorrentMetadata getTorrentMetadata() throws IOException;
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentsStorage.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentsStorage.java
new file mode 100644
index 0000000..d055856
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/TorrentsStorage.java
@@ -0,0 +1,177 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.Pair;
+import com.turn.ttorrent.common.TorrentUtils;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+// 客户端获取
+//负责管理两类 torrent:活跃的(正在进行下载或上传的)和已加载的(已经被解析并加载进内存但可能未被活跃使用的) torrent。
+//该类提供了方法来添加、获取、移除、检查这些 torrent,同时支持高效的并发访问,确保线程安全。
+public class TorrentsStorage {
+
+  private final ReadWriteLock readWriteLock;
+  private final Map<String, SharedTorrent> activeTorrents;
+  private final Map<String, LoadedTorrent> loadedTorrents;
+
+  public TorrentsStorage() {
+    readWriteLock = new ReentrantReadWriteLock();
+    activeTorrents = new HashMap<String, SharedTorrent>();
+    loadedTorrents = new HashMap<String, LoadedTorrent>();
+  }
+
+  //根据hash查找是否有对应种子
+  public boolean hasTorrent(String hash) {
+    try {
+      readWriteLock.readLock().lock();
+      return loadedTorrents.containsKey(hash);
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  //获取已加载的torrent
+  public LoadedTorrent getLoadedTorrent(String hash) {
+    try {
+      readWriteLock.readLock().lock();
+      return loadedTorrents.get(hash);
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  // 处理peer断开连接的情况 种子不活跃了在此处进行操作
+  public void peerDisconnected(String torrentHash) {
+    final SharedTorrent torrent;
+    try {
+      readWriteLock.writeLock().lock();
+      torrent = activeTorrents.get(torrentHash);
+      if (torrent == null) return;
+
+      boolean isTorrentFinished = torrent.isFinished();
+      if (torrent.getDownloadersCount() == 0 && isTorrentFinished) {
+        activeTorrents.remove(torrentHash);
+      } else {
+        return;
+      }
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+    torrent.close();
+  }
+
+  //获取活跃的torrent
+  public SharedTorrent getTorrent(String hash) {
+    try {
+      readWriteLock.readLock().lock();
+      return activeTorrents.get(hash);
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  // 将已经加载的种子加入
+  public void addTorrent(String hash, LoadedTorrent torrent) {
+    try {
+      readWriteLock.writeLock().lock();
+      loadedTorrents.put(hash, torrent);
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+  }
+
+  public SharedTorrent putIfAbsentActiveTorrent(String hash, SharedTorrent torrent) {
+    try {
+      readWriteLock.writeLock().lock();
+      final SharedTorrent old = activeTorrents.get(hash);
+      if (old != null) return old;
+
+      return activeTorrents.put(hash, torrent);
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+  }
+
+  public Pair<SharedTorrent, LoadedTorrent> remove(String hash) {
+    final Pair<SharedTorrent, LoadedTorrent> result;
+    try {
+      readWriteLock.writeLock().lock();
+      final SharedTorrent sharedTorrent = activeTorrents.remove(hash);
+      final LoadedTorrent loadedTorrent = loadedTorrents.remove(hash);
+      result = new Pair<SharedTorrent, LoadedTorrent>(sharedTorrent, loadedTorrent);
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+    if (result.second() != null) {
+      try {
+        result.second().getPieceStorage().close();
+      } catch (IOException ignored) {
+      }
+    }
+    if (result.first() != null) {
+      result.first().closeFully();
+    }
+    return result;
+  }
+
+  // 获取活跃的种子
+  public List<SharedTorrent> activeTorrents() {
+    try {
+      readWriteLock.readLock().lock();
+      return new ArrayList<SharedTorrent>(activeTorrents.values());
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  public List<AnnounceableInformation> announceableTorrents() {
+    List<AnnounceableInformation> result = new ArrayList<AnnounceableInformation>();
+    try {
+      readWriteLock.readLock().lock();
+      for (LoadedTorrent loadedTorrent : loadedTorrents.values()) {
+        AnnounceableInformation announceableInformation = loadedTorrent.createAnnounceableInformation();
+        if (TorrentUtils.isTrackerLessInfo(announceableInformation)) continue;
+        result.add(announceableInformation);
+      }
+      return result;
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  public List<LoadedTorrent> getLoadedTorrents() {
+    try {
+      readWriteLock.readLock().lock();
+      return new ArrayList<LoadedTorrent>(loadedTorrents.values());
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  public void clear() {
+    final Collection<SharedTorrent> sharedTorrents;
+    final Collection<LoadedTorrent> loadedTorrents;
+    try {
+      readWriteLock.writeLock().lock();
+      sharedTorrents = new ArrayList<SharedTorrent>(activeTorrents.values());
+      loadedTorrents = new ArrayList<LoadedTorrent>(this.loadedTorrents.values());
+      this.loadedTorrents.clear();
+      activeTorrents.clear();
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+    for (SharedTorrent sharedTorrent : sharedTorrents) {
+      sharedTorrent.closeFully();
+    }
+    for (LoadedTorrent loadedTorrent : loadedTorrents) {
+      try {
+        loadedTorrent.getPieceStorage().close();
+      } catch (IOException ignored) {
+      }
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/Announce.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/Announce.java
new file mode 100644
index 0000000..ad38700
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/Announce.java
@@ -0,0 +1,315 @@
+/**
+ * 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.client.announce;
+
+import com.turn.ttorrent.client.Context;
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import org.slf4j.Logger;
+
+import java.net.ConnectException;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.net.UnknownServiceException;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * BitTorrent announce sub-system.
+ * <p/>
+ * <p>
+ * A BitTorrent client must check-in to the torrent's tracker(s) to get peers
+ * and to report certain events.
+ * </p>
+ * <p/>
+ * <p>
+ * This Announce class implements a periodic announce request thread that will
+ * notify announce request event listeners for each tracker response.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @see com.turn.ttorrent.common.protocol.TrackerMessage
+ */
+public class Announce implements Runnable {
+
+  protected static final Logger logger =
+          TorrentLoggerFactory.getLogger(Announce.class);
+
+  private List<Peer> myPeers;
+  private final TrackerClientFactory myTrackerClientFactory;
+
+  /**
+   * The tiers of tracker clients matching the tracker URIs defined in the
+   * torrent.
+   */
+  private final ConcurrentMap<String, TrackerClient> clients;
+  private final Context myContext;
+
+  /**
+   * Announce thread and control.
+   */
+  private Thread thread;
+  private volatile boolean stop;
+  private boolean forceStop;
+
+  /**
+   * Announce interval.
+   */
+  private int myAnnounceInterval;
+  private TrackerClient myDefaultTracker;
+
+  /**
+   * Initialize the base announce class members for the announcer.
+   */
+  public Announce(Context context, TrackerClientFactory trackerClientFactory) {
+    this.clients = new ConcurrentHashMap<String, TrackerClient>();
+    this.thread = null;
+    myTrackerClientFactory = trackerClientFactory;
+    myContext = context;
+    myPeers = new CopyOnWriteArrayList<Peer>();
+  }
+
+  public void forceAnnounce(AnnounceableInformation torrent, AnnounceResponseListener listener, AnnounceRequestMessage.RequestEvent event) throws UnknownServiceException, UnknownHostException {
+    URI trackerUrl = URI.create(torrent.getAnnounce());
+    TrackerClient client = this.clients.get(trackerUrl.toString());
+    try {
+      if (client == null) {
+        client = myTrackerClientFactory.createTrackerClient(myPeers, trackerUrl);
+        client.register(listener);
+        this.clients.put(trackerUrl.toString(), client);
+      }
+      client.announceAllInterfaces(event, false, torrent);
+    } catch (AnnounceException e) {
+      logger.info(String.format("Unable to force announce torrent %s on tracker %s.", torrent.getHexInfoHash(), String.valueOf(trackerUrl)));
+      logger.debug(String.format("Unable to force announce torrent %s on tracker %s.", torrent.getHexInfoHash(), String.valueOf(trackerUrl)), e);
+    }
+  }
+
+  /**
+   * Start the announce request thread.
+   */
+  public void start(final URI defaultTrackerURI, final AnnounceResponseListener listener, final Peer[] peers, final int announceInterval) {
+    myAnnounceInterval = announceInterval;
+    myPeers.addAll(Arrays.asList(peers));
+    if (defaultTrackerURI != null) {
+      try {
+        myDefaultTracker = myTrackerClientFactory.createTrackerClient(myPeers, defaultTrackerURI);
+        myDefaultTracker.register(listener);
+        this.clients.put(defaultTrackerURI.toString(), myDefaultTracker);
+      } catch (Exception e) {
+      }
+    } else {
+      myDefaultTracker = null;
+    }
+
+    this.stop = false;
+    this.forceStop = false;
+
+    if (this.thread == null || !this.thread.isAlive()) {
+      this.thread = new Thread(this);
+      this.thread.setName("torrent tracker announce thread");
+      this.thread.start();
+    }
+  }
+
+  /**
+   * Set the announce interval.
+   */
+  public void setAnnounceInterval(int announceInterval) {
+    if (announceInterval <= 0) {
+      this.stop(true);
+      return;
+    }
+
+    if (this.myAnnounceInterval == announceInterval) {
+      return;
+    }
+
+    logger.trace("Setting announce interval to {}s per tracker request.",
+            announceInterval);
+    this.myAnnounceInterval = announceInterval;
+  }
+
+  /**
+   * Stop the announce thread.
+   * <p/>
+   * <p>
+   * One last 'stopped' announce event might be sent to the tracker to
+   * announce we're going away, depending on the implementation.
+   * </p>
+   */
+  public void stop() {
+
+    this.stop = true;
+
+    if (this.thread != null && this.thread.isAlive()) {
+      this.thread.interrupt();
+
+      for (TrackerClient client : this.clients.values()) {
+        client.close();
+      }
+
+      try {
+        this.thread.join();
+      } catch (InterruptedException ie) {
+        // Ignore
+      }
+    }
+    this.myPeers.clear();
+
+    this.thread = null;
+  }
+
+  /**
+   * Main announce loop.
+   * <p/>
+   * <p>
+   * The announce thread starts by making the initial 'started' announce
+   * request to register on the tracker and get the announce interval value.
+   * Subsequent announce requests are ordinary, event-less, periodic requests
+   * for peers.
+   * </p>
+   * <p/>
+   * <p>
+   * Unless forcefully stopped, the announce thread will terminate by sending
+   * a 'stopped' announce request before stopping.
+   * </p>
+   */
+  @Override
+  public void run() {
+    logger.info("Starting announce loop...");
+
+
+    while (!this.stop && !Thread.currentThread().isInterrupted()) {
+
+      final List<AnnounceableInformation> announceableInformationList = myContext.getTorrentsStorage().announceableTorrents();
+      logger.debug("Starting announce for {} torrents", announceableInformationList.size());
+      announceAllTorrents(announceableInformationList, AnnounceRequestMessage.RequestEvent.NONE);
+      try {
+        Thread.sleep(this.myAnnounceInterval * 1000);
+      } catch (InterruptedException ie) {
+        break;
+      }
+    }
+
+    announceAllTorrents(myContext.getTorrentsStorage().announceableTorrents(), AnnounceRequestMessage.RequestEvent.STOPPED);
+
+    logger.info("Exited announce loop.");
+  }
+
+  private void defaultAnnounce(List<AnnounceableInformation> torrentsForAnnounce) {
+    for (AnnounceableInformation torrent : torrentsForAnnounce) {
+      if (this.stop || Thread.currentThread().isInterrupted()) {
+        break;
+      }
+      try {
+        TrackerClient trackerClient = this.getCurrentTrackerClient(torrent);
+        if (trackerClient != null) {
+          trackerClient.announceAllInterfaces(AnnounceRequestMessage.RequestEvent.NONE, false, torrent);
+        } else {
+          logger.warn("Tracker client for {} is null. Torrent is not announced on tracker", torrent.getHexInfoHash());
+        }
+      } catch (Exception e) {
+        logger.info(e.getMessage());
+        logger.debug(e.getMessage(), e);
+      }
+    }
+  }
+
+  private void announceAllTorrents(List<AnnounceableInformation> announceableInformationList, AnnounceRequestMessage.RequestEvent event) {
+
+    logger.debug("Started multi announce. Event {}, torrents {}", event, announceableInformationList);
+    final Map<String, List<AnnounceableInformation>> torrentsGroupingByAnnounceUrl = new HashMap<String, List<AnnounceableInformation>>();
+
+    for (AnnounceableInformation torrent : announceableInformationList) {
+      final URI uriForTorrent = getURIForTorrent(torrent);
+      if (uriForTorrent == null) continue;
+      String torrentURI = uriForTorrent.toString();
+      List<AnnounceableInformation> sharedTorrents = torrentsGroupingByAnnounceUrl.get(torrentURI);
+      if (sharedTorrents == null) {
+        sharedTorrents = new ArrayList<AnnounceableInformation>();
+        torrentsGroupingByAnnounceUrl.put(torrentURI, sharedTorrents);
+      }
+      sharedTorrents.add(torrent);
+    }
+
+    List<AnnounceableInformation> unannouncedTorrents = new ArrayList<AnnounceableInformation>();
+    for (Map.Entry<String, List<AnnounceableInformation>> e : torrentsGroupingByAnnounceUrl.entrySet()) {
+      TrackerClient trackerClient = this.clients.get(e.getKey());
+      if (trackerClient != null) {
+        try {
+          trackerClient.multiAnnounce(event, false, e.getValue(), myPeers);
+        } catch (AnnounceException t) {
+          LoggerUtils.warnAndDebugDetails(logger, "problem in multi announce {}", t.getMessage(), t);
+          unannouncedTorrents.addAll(e.getValue());
+        } catch (ConnectException t) {
+          LoggerUtils.warnWithMessageAndDebugDetails(logger, "Cannot connect to the tracker {}", e.getKey(), t);
+          logger.debug("next torrents contain {} in tracker list. {}", e.getKey(), e.getValue());
+        }
+      } else {
+        logger.warn("Tracker client for {} is null. Torrents are not announced on tracker", e.getKey());
+        if (e.getKey() == null || e.getKey().isEmpty()) {
+          for (AnnounceableInformation announceableInformation : e.getValue()) {
+            myContext.getTorrentsStorage().remove(announceableInformation.getHexInfoHash());
+          }
+        }
+      }
+    }
+    if (unannouncedTorrents.size() > 0) {
+      defaultAnnounce(unannouncedTorrents);
+    }
+  }
+
+  /**
+   * Returns the current tracker client used for announces.
+   */
+  public TrackerClient getCurrentTrackerClient(AnnounceableInformation torrent) {
+    final URI uri = getURIForTorrent(torrent);
+    if (uri == null) return null;
+    return this.clients.get(uri.toString());
+  }
+
+  private URI getURIForTorrent(AnnounceableInformation torrent) {
+    List<List<String>> announceList = torrent.getAnnounceList();
+    if (announceList.size() == 0) return null;
+    List<String> uris = announceList.get(0);
+    if (uris.size() == 0) return null;
+    return URI.create(uris.get(0));
+  }
+
+  public URI getDefaultTrackerURI() {
+    if (myDefaultTracker == null) {
+      return null;
+    }
+    return myDefaultTracker.getTrackerURI();
+  }
+
+  /**
+   * Stop the announce thread.
+   *
+   * @param hard Whether to force stop the announce thread or not, i.e. not
+   *             send the final 'stopped' announce request or not.
+   */
+  private void stop(boolean hard) {
+    this.forceStop = hard;
+    this.stop();
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/AnnounceException.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/AnnounceException.java
new file mode 100644
index 0000000..716877f
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/AnnounceException.java
@@ -0,0 +1,39 @@
+/**
+ * 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.client.announce;
+
+
+/**
+ * Exception thrown when an announce request failed.
+ *
+ * @author mpetazzoni
+ */
+public class AnnounceException extends Exception {
+
+  private static final long serialVersionUID = -1;
+
+  public AnnounceException(String message) {
+    super(message);
+  }
+
+  public AnnounceException(Throwable cause) {
+    super(cause);
+  }
+
+  public AnnounceException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/AnnounceResponseListener.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/AnnounceResponseListener.java
new file mode 100644
index 0000000..6d83cbd
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/AnnounceResponseListener.java
@@ -0,0 +1,47 @@
+/**
+ * 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.client.announce;
+
+import com.turn.ttorrent.common.Peer;
+
+import java.util.EventListener;
+import java.util.List;
+
+
+/**
+ * EventListener interface for objects that want to receive tracker responses.
+ *
+ * @author mpetazzoni
+ */
+public interface AnnounceResponseListener extends EventListener {
+
+  /**
+   * Handle an announce response event.
+   *
+   * @param interval   The announce interval requested by the tracker.
+   * @param complete   The number of seeders on this torrent.
+   * @param incomplete The number of leechers on this torrent.
+   */
+  void handleAnnounceResponse(int interval, int complete, int incomplete, String hexInfoHash);
+
+  /**
+   * Handle the discovery of new peers.
+   *
+   * @param peers The list of peers discovered (from the announce response or
+   *              any other means like DHT/PEX, etc.).
+   */
+  void handleDiscoveredPeers(List<Peer> peers, String hexInfoHash);
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/HTTPTrackerClient.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/HTTPTrackerClient.java
new file mode 100644
index 0000000..43b5b5d
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/HTTPTrackerClient.java
@@ -0,0 +1,335 @@
+/**
+ * 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.client.announce;
+
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage.MessageValidationException;
+import com.turn.ttorrent.common.protocol.http.HTTPAnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPAnnounceResponseMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPTrackerMessage;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Announcer for HTTP trackers.
+ *
+ * @author mpetazzoni
+ * @see <a href="http://wiki.theory.org/BitTorrentSpecification#Tracker_Request_Parameters">BitTorrent tracker request specification</a>
+ */
+public class HTTPTrackerClient extends TrackerClient {
+
+  protected static final Logger logger =
+          TorrentLoggerFactory.getLogger(HTTPTrackerClient.class);
+
+  /**
+   * Create a new HTTP announcer for the given torrent.
+   *
+   * @param peers Our own peer specification.
+   */
+  public HTTPTrackerClient(List<Peer> peers, URI tracker) {
+    super(peers, tracker);
+  }
+
+  /**
+   * Build, send and process a tracker announce request.
+   *
+   * <p>
+   * This function first builds an announce request for the specified event
+   * with all the required parameters. Then, the request is made to the
+   * tracker and the response analyzed.
+   * </p>
+   *
+   * <p>
+   * All registered {@link AnnounceResponseListener} objects are then fired
+   * with the decoded payload.
+   * </p>
+   *
+   * @param event         The announce event type (can be AnnounceEvent.NONE for
+   *                      periodic updates).
+   * @param inhibitEvents Prevent event listeners from being notified.
+   * @param torrentInfo
+   */
+  public void announce(final AnnounceRequestMessage.RequestEvent event,
+                       boolean inhibitEvents, final AnnounceableInformation torrentInfo, final List<Peer> adresses) throws AnnounceException {
+    logAnnounceRequest(event, torrentInfo);
+
+    final List<HTTPTrackerMessage> trackerResponses = new ArrayList<HTTPTrackerMessage>();
+    for (final Peer address : adresses) {
+      final URL target = encodeAnnounceToURL(event, torrentInfo, address);
+      try {
+        sendAnnounce(target, "GET", new ResponseParser() {
+          @Override
+          public void parse(InputStream inputStream, int responseCode) throws IOException, MessageValidationException {
+            if (responseCode != 200) {
+              logger.info("received not http 200 code from tracker for request " + target);
+              return;
+            }
+            trackerResponses.add(HTTPTrackerMessage.parse(inputStream));
+          }
+        });
+      } catch (ConnectException e) {
+        throw new AnnounceException(e.getMessage(), e);
+      }
+    }
+    // we process only first request:
+    if (trackerResponses.size() > 0) {
+      final HTTPTrackerMessage message = trackerResponses.get(0);
+      this.handleTrackerAnnounceResponse(message, inhibitEvents, torrentInfo.getHexInfoHash());
+    }
+  }
+
+  @Override
+  protected void multiAnnounce(AnnounceRequestMessage.RequestEvent event,
+                               boolean inhibitEvent,
+                               final List<? extends AnnounceableInformation> torrents,
+                               List<Peer> addresses) throws AnnounceException, ConnectException {
+    List<List<HTTPTrackerMessage>> trackerResponses = new ArrayList<List<HTTPTrackerMessage>>();
+
+    URL trackerUrl;
+    try {
+      trackerUrl = this.tracker.toURL();
+    } catch (MalformedURLException e) {
+      throw new AnnounceException("Invalid tracker URL " + this.tracker, e);
+    }
+
+    for (final Peer address : addresses) {
+      StringBuilder body = new StringBuilder();
+      for (final AnnounceableInformation torrentInfo : torrents) {
+        body.append(encodeAnnounceToURL(event, torrentInfo, address)).append("\n");
+      }
+      final List<HTTPTrackerMessage> responsesForCurrentIp = new ArrayList<HTTPTrackerMessage>();
+      final String bodyStr = body.substring(0, body.length() - 1);
+      sendAnnounce(trackerUrl, bodyStr, "POST", new ResponseParser() {
+        @Override
+        public void parse(InputStream inputStream, int responseCode) throws IOException, MessageValidationException {
+
+          if (responseCode != 200) {
+            logger.info("received {} code from tracker for multi announce request.", responseCode);
+            logger.debug(bodyStr);
+            return;
+          }
+
+          final BEValue bdecode = BDecoder.bdecode(inputStream);
+          if (bdecode == null) {
+            logger.info("tracker sent bad response for multi announce message.");
+            logger.debug(bodyStr);
+            return;
+          }
+          final List<BEValue> list = bdecode.getList();
+          for (BEValue value : list) {
+            responsesForCurrentIp.add(HTTPTrackerMessage.parse(value));
+          }
+        }
+      });
+      if (!responsesForCurrentIp.isEmpty()) {
+        trackerResponses.add(responsesForCurrentIp);
+      }
+    }
+    // we process only first request:
+    if (trackerResponses.size() > 0) {
+      final List<HTTPTrackerMessage> messages = trackerResponses.get(0);
+      for (HTTPTrackerMessage message : messages) {
+
+        if (!(message instanceof HTTPAnnounceResponseMessage)) {
+          logger.info("Incorrect instance of message {}. Skipping...", message);
+          continue;
+        }
+
+        final String hexInfoHash = ((HTTPAnnounceResponseMessage) message).getHexInfoHash();
+        try {
+          this.handleTrackerAnnounceResponse(message, inhibitEvent, hexInfoHash);
+        } catch (AnnounceException e) {
+          LoggerUtils.errorAndDebugDetails(logger, "Unable to process tracker response {}", message, e);
+        }
+      }
+    }
+  }
+
+  private URL encodeAnnounceToURL(AnnounceRequestMessage.RequestEvent event, AnnounceableInformation torrentInfo, Peer peer) throws AnnounceException {
+    URL result;
+    try {
+      HTTPAnnounceRequestMessage request = this.buildAnnounceRequest(event, torrentInfo, peer);
+      result = request.buildAnnounceURL(this.tracker.toURL());
+    } catch (MalformedURLException mue) {
+      throw new AnnounceException("Invalid announce URL (" +
+              mue.getMessage() + ")", mue);
+    } catch (MessageValidationException mve) {
+      throw new AnnounceException("Announce request creation violated " +
+              "expected protocol (" + mve.getMessage() + ")", mve);
+    } catch (IOException ioe) {
+      throw new AnnounceException("Error building announce request (" +
+              ioe.getMessage() + ")", ioe);
+    }
+    return result;
+  }
+
+  private void sendAnnounce(final URL url, final String method, ResponseParser parser)
+          throws AnnounceException, ConnectException {
+    sendAnnounce(url, "", method, parser);
+  }
+
+  private void sendAnnounce(final URL url, final String body, final String method, ResponseParser parser)
+          throws AnnounceException, ConnectException {
+    HttpURLConnection conn = null;
+    InputStream in = null;
+    try {
+      conn = (HttpURLConnection) openConnectionCheckRedirects(url, body, method);
+      in = conn.getInputStream();
+    } catch (IOException ioe) {
+      if (conn != null) {
+        in = conn.getErrorStream();
+      }
+    }
+
+    // At this point if the input stream is null it means we have neither a
+    // response body nor an error stream from the server. No point in going
+    // any further.
+    if (in == null) {
+      throw new ConnectException("No response or unreachable tracker!");
+    }
+
+    try {
+      parser.parse(in, conn.getResponseCode());
+    } catch (IOException ioe) {
+      throw new AnnounceException("Error reading tracker response!", ioe);
+    } catch (MessageValidationException mve) {
+      throw new AnnounceException("Tracker message violates expected " +
+              "protocol (" + mve.getMessage() + ")", mve);
+    } finally {
+      // Make sure we close everything down at the end to avoid resource
+      // leaks.
+      try {
+        in.close();
+      } catch (IOException ioe) {
+        logger.info("Problem ensuring error stream closed!");
+        logger.debug("Problem ensuring error stream closed!", ioe);
+      }
+
+      // This means trying to close the error stream as well.
+      InputStream err = conn.getErrorStream();
+      if (err != null) {
+        try {
+          err.close();
+        } catch (IOException ioe) {
+          logger.info("Problem ensuring error stream closed!");
+          logger.debug("Problem ensuring error stream closed!", ioe);
+        }
+      }
+    }
+  }
+
+  private URLConnection openConnectionCheckRedirects(URL url, String body, String method) throws IOException {
+    boolean needRedirect;
+    int redirects = 0;
+    URLConnection connection = url.openConnection();
+    boolean firstIteration = true;
+    do {
+      needRedirect = false;
+      connection.setConnectTimeout(10000);
+      connection.setReadTimeout(10000);
+      HttpURLConnection http = null;
+      if (connection instanceof HttpURLConnection) {
+        http = (HttpURLConnection) connection;
+        http.setInstanceFollowRedirects(false);
+      }
+      if (http != null) {
+
+        if (firstIteration) {
+          firstIteration = false;
+          http.setRequestProperty("Content-Type", "text/plain; charset=UTF-8");
+          http.setRequestMethod(method);
+          if (!body.isEmpty()) {
+            connection.setDoOutput(true);
+            connection.getOutputStream().write(body.getBytes("UTF-8"));
+          }
+        }
+
+        int stat = http.getResponseCode();
+        if (stat >= 300 && stat <= 307 && stat != 306 &&
+                stat != HttpURLConnection.HTTP_NOT_MODIFIED) {
+          URL base = http.getURL();
+          String newLocation = http.getHeaderField("Location");
+          URL target = newLocation == null ? null : new URL(base, newLocation);
+          http.disconnect();
+          // Redirection should be allowed only for HTTP and HTTPS
+          // and should be limited to 5 redirections at most.
+          if (redirects >= 5) {
+            throw new IOException("too many redirects");
+          }
+          if (target == null || !(target.getProtocol().equals("http")
+                  || target.getProtocol().equals("https"))) {
+            throw new IOException("illegal URL redirect or protocol");
+          }
+          needRedirect = true;
+          connection = target.openConnection();
+          redirects++;
+        }
+      }
+    }
+    while (needRedirect);
+    return connection;
+  }
+
+  /**
+   * Build the announce request tracker message.
+   *
+   * @param event The announce event (can be <tt>NONE</tt> or <em>null</em>)
+   * @return Returns an instance of a {@link HTTPAnnounceRequestMessage}
+   * that can be used to generate the fully qualified announce URL, with
+   * parameters, to make the announce request.
+   * @throws UnsupportedEncodingException
+   * @throws IOException
+   * @throws MessageValidationException
+   */
+  private HTTPAnnounceRequestMessage buildAnnounceRequest(
+          AnnounceRequestMessage.RequestEvent event, AnnounceableInformation torrentInfo, Peer peer)
+          throws IOException,
+          MessageValidationException {
+    // Build announce request message
+    final long uploaded = torrentInfo.getUploaded();
+    final long downloaded = torrentInfo.getDownloaded();
+    final long left = torrentInfo.getLeft();
+    return HTTPAnnounceRequestMessage.craft(
+            torrentInfo.getInfoHash(),
+            peer.getPeerIdArray(),
+            peer.getPort(),
+            uploaded,
+            downloaded,
+            left,
+            true, false, event,
+            peer.getIp(),
+            AnnounceRequestMessage.DEFAULT_NUM_WANT);
+  }
+
+  private interface ResponseParser {
+
+    void parse(InputStream inputStream, int responseCode) throws IOException, MessageValidationException;
+
+  }
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java
new file mode 100644
index 0000000..1baddf6
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClient.java
@@ -0,0 +1,215 @@
+/**
+ * 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.client.announce;
+
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.AnnounceResponseMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage.ErrorMessage;
+import org.slf4j.Logger;
+
+import java.net.ConnectException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class TrackerClient {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(TrackerClient.class);
+
+
+  /**
+   * The set of listeners to announce request answers.
+   */
+  private final Set<AnnounceResponseListener> listeners;
+
+  protected final List<Peer> myAddress;
+  protected final URI tracker;
+
+  public TrackerClient(final List<Peer> peers, final URI tracker) {
+    this.listeners = new HashSet<AnnounceResponseListener>();
+    myAddress = peers;
+    this.tracker = tracker;
+  }
+
+  /**
+   * Register a new announce response listener.
+   *
+   * @param listener The listener to register on this announcer events.
+   */
+  public void register(AnnounceResponseListener listener) {
+    this.listeners.add(listener);
+  }
+
+  /**
+   * Returns the URI this tracker clients connects to.
+   */
+  public URI getTrackerURI() {
+    return this.tracker;
+  }
+
+  public void announceAllInterfaces(final AnnounceRequestMessage.RequestEvent event,
+                                    boolean inhibitEvent, final AnnounceableInformation torrent) throws AnnounceException {
+    try {
+      announce(event, inhibitEvent, torrent, myAddress);
+    } catch (AnnounceException e) {
+      throw new AnnounceException(String.format("Unable to announce tracker %s event %s for torrent %s and peers %s. Reason %s",
+              getTrackerURI(), event.getEventName(), torrent.getHexInfoHash(), Arrays.toString(myAddress.toArray()), e.getMessage()), e);
+    }
+  }
+
+  /**
+   * Build, send and process a tracker announce request.
+   *
+   * <p>
+   * This function first builds an announce request for the specified event
+   * with all the required parameters. Then, the request is made to the
+   * tracker and the response analyzed.
+   * </p>
+   *
+   * <p>
+   * All registered {@link AnnounceResponseListener} objects are then fired
+   * with the decoded payload.
+   * </p>
+   *
+   * @param event        The announce event type (can be AnnounceEvent.NONE for
+   *                     periodic updates).
+   * @param inhibitEvent Prevent event listeners from being notified.
+   * @param torrent
+   */
+  protected abstract void announce(final AnnounceRequestMessage.RequestEvent event,
+                                   boolean inhibitEvent, final AnnounceableInformation torrent, final List<Peer> peer) throws AnnounceException;
+
+  protected abstract void multiAnnounce(final AnnounceRequestMessage.RequestEvent event,
+                                        boolean inhibitEvent,
+                                        final List<? extends AnnounceableInformation> torrents,
+                                        final List<Peer> peer) throws AnnounceException, ConnectException;
+
+  protected void logAnnounceRequest(AnnounceRequestMessage.RequestEvent event, AnnounceableInformation torrent) {
+    if (event != AnnounceRequestMessage.RequestEvent.NONE) {
+      logger.debug("Announcing {} to tracker with {}U/{}D/{}L bytes...",
+              new Object[]{
+                      this.formatAnnounceEvent(event),
+                      torrent.getUploaded(),
+                      torrent.getDownloaded(),
+                      torrent.getLeft()
+              });
+    } else {
+      logger.debug("Simply announcing to tracker with {}U/{}D/{}L bytes...",
+              new Object[]{
+                      torrent.getUploaded(),
+                      torrent.getDownloaded(),
+                      torrent.getLeft()
+              });
+    }
+  }
+
+  /**
+   * Close any opened announce connection.
+   *
+   * <p>
+   * This method is called to make sure all connections
+   * are correctly closed when the announce thread is asked to stop.
+   * </p>
+   */
+  protected void close() {
+    // Do nothing by default, but can be overloaded.
+  }
+
+  /**
+   * Formats an announce event into a usable string.
+   */
+  protected String formatAnnounceEvent(
+          AnnounceRequestMessage.RequestEvent event) {
+    return AnnounceRequestMessage.RequestEvent.NONE.equals(event)
+            ? ""
+            : String.format(" %s", event.name());
+  }
+
+  /**
+   * Handle the announce response from the tracker.
+   *
+   * <p>
+   * Analyzes the response from the tracker and acts on it. If the response
+   * is an error, it is logged. Otherwise, the announce response is used
+   * to fire the corresponding announce and peer events to all announce
+   * listeners.
+   * </p>
+   *
+   * @param message       The incoming {@link TrackerMessage}.
+   * @param inhibitEvents Whether or not to prevent events from being fired.
+   */
+  protected void handleTrackerAnnounceResponse(TrackerMessage message,
+                                               boolean inhibitEvents, String hexInfoHash) throws AnnounceException {
+    if (message instanceof ErrorMessage) {
+      ErrorMessage error = (ErrorMessage) message;
+      throw new AnnounceException(error.getReason());
+    }
+
+    if (!(message instanceof AnnounceResponseMessage)) {
+      throw new AnnounceException("Unexpected tracker message type " +
+              message.getType().name() + "!");
+    }
+
+
+    AnnounceResponseMessage response =
+            (AnnounceResponseMessage) message;
+
+    this.fireAnnounceResponseEvent(
+            response.getComplete(),
+            response.getIncomplete(),
+            response.getInterval(),
+            hexInfoHash);
+
+    if (inhibitEvents) {
+      return;
+    }
+
+    this.fireDiscoveredPeersEvent(
+            response.getPeers(),
+            hexInfoHash);
+  }
+
+  /**
+   * Fire the announce response event to all listeners.
+   *
+   * @param complete   The number of seeders on this torrent.
+   * @param incomplete The number of leechers on this torrent.
+   * @param interval   The announce interval requested by the tracker.
+   */
+  protected void fireAnnounceResponseEvent(int complete, int incomplete, int interval, String hexInfoHash) {
+    for (AnnounceResponseListener listener : this.listeners) {
+      listener.handleAnnounceResponse(interval, complete, incomplete, hexInfoHash);
+    }
+  }
+
+  /**
+   * Fire the new peer discovery event to all listeners.
+   *
+   * @param peers The list of peers discovered.
+   */
+  protected void fireDiscoveredPeersEvent(List<Peer> peers, String hexInfoHash) {
+    for (AnnounceResponseListener listener : this.listeners) {
+      listener.handleDiscoveredPeers(peers, hexInfoHash);
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClientFactory.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClientFactory.java
new file mode 100644
index 0000000..5e61e36
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClientFactory.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.client.announce;
+
+import com.turn.ttorrent.common.Peer;
+
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.net.UnknownServiceException;
+import java.util.List;
+
+public interface TrackerClientFactory {
+
+  /**
+   * Create a {@link TrackerClient} announcing to the given tracker address.
+   *
+   * @param peers   The list peer the tracker client will announce on behalf of.
+   * @param tracker The tracker address as a {@link java.net.URI}.
+   * @throws UnknownHostException    If the tracker address is invalid.
+   * @throws UnknownServiceException If the tracker protocol is not supported.
+   */
+  TrackerClient createTrackerClient(List<Peer> peers, URI tracker) throws UnknownHostException, UnknownServiceException;
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClientFactoryImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClientFactoryImpl.java
new file mode 100644
index 0000000..542bda0
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/TrackerClientFactoryImpl.java
@@ -0,0 +1,41 @@
+/*
+ * 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.client.announce;
+
+import com.turn.ttorrent.common.Peer;
+
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.net.UnknownServiceException;
+import java.util.List;
+
+public class TrackerClientFactoryImpl implements TrackerClientFactory {
+
+  @Override
+  public TrackerClient createTrackerClient(List<Peer> peers, URI tracker) throws UnknownHostException, UnknownServiceException {
+    String scheme = tracker.getScheme();
+
+    if ("http".equals(scheme) || "https".equals(scheme)) {
+      return new HTTPTrackerClient(peers, tracker);
+    } else if ("udp".equals(scheme)) {
+      return new UDPTrackerClient(peers, tracker);
+    }
+
+    throw new UnknownServiceException(
+            "Unsupported announce scheme: " + scheme + "!");
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java
new file mode 100644
index 0000000..0549908
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/announce/UDPTrackerClient.java
@@ -0,0 +1,373 @@
+/**
+ * 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.client.announce;
+
+import com.turn.ttorrent.common.AnnounceableInformation;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage.ConnectionResponseMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage.ErrorMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage.MessageValidationException;
+import com.turn.ttorrent.common.protocol.udp.UDPAnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.udp.UDPConnectRequestMessage;
+import com.turn.ttorrent.common.protocol.udp.UDPConnectResponseMessage;
+import com.turn.ttorrent.common.protocol.udp.UDPTrackerMessage;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.net.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.UnsupportedAddressTypeException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Announcer for UDP trackers.
+ *
+ * <p>
+ * The UDP tracker protocol requires a two-step announce request/response
+ * exchange where the peer is first required to establish a "connection"
+ * with the tracker by sending a connection request message and retreiving
+ * a connection ID from the tracker to use in the following announce
+ * request messages (valid for 2 minutes).
+ * </p>
+ *
+ * <p>
+ * It also contains a backing-off retry mechanism (on a 15*2^n seconds
+ * scheme), in which if the announce request times-out for more than the
+ * connection ID validity period, another connection request/response
+ * exchange must be made before attempting to retransmit the announce
+ * request.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public class UDPTrackerClient extends TrackerClient {
+
+  protected static final Logger logger =
+          TorrentLoggerFactory.getLogger(UDPTrackerClient.class);
+
+  /**
+   * Back-off timeout uses 15 * 2 ^ n formula.
+   */
+  private static final int UDP_BASE_TIMEOUT_SECONDS = 15;
+
+  /**
+   * We don't try more than 8 times (3840 seconds, as per the formula defined
+   * for the backing-off timeout.
+   *
+   * @see #UDP_BASE_TIMEOUT_SECONDS
+   */
+  private static final int UDP_MAX_TRIES = 8;
+
+  /**
+   * For STOPPED announce event, we don't want to be bothered with waiting
+   * that long. We'll try once and bail-out early.
+   */
+  private static final int UDP_MAX_TRIES_ON_STOPPED = 1;
+
+  /**
+   * Maximum UDP packet size expected, in bytes.
+   *
+   * The biggest packet in the exchange is the announce response, which in 20
+   * bytes + 6 bytes per peer. Common numWant is 50, so 20 + 6 * 50 = 320.
+   * With headroom, we'll ask for 512 bytes.
+   */
+  private static final int UDP_PACKET_LENGTH = 512;
+
+  private final InetSocketAddress address;
+  private final Random random;
+
+  private DatagramSocket socket;
+  private Date connectionExpiration;
+  private long connectionId;
+  private int transactionId;
+  private boolean stop;
+
+  private enum State {
+    CONNECT_REQUEST,
+    ANNOUNCE_REQUEST
+  }
+
+  /**
+   *
+   */
+  protected UDPTrackerClient(List<Peer> peers, URI tracker)
+          throws UnknownHostException {
+    super(peers, tracker);
+
+    /**
+     * The UDP announce request protocol only supports IPv4
+     *
+     * @see http://bittorrent.org/beps/bep_0015.html#ipv6
+     */
+    for (Peer peer : peers) {
+      if (!(InetAddress.getByName(peer.getIp()) instanceof Inet4Address)) {
+        throw new UnsupportedAddressTypeException();
+      }
+    }
+
+    this.address = new InetSocketAddress(
+            tracker.getHost(),
+            tracker.getPort());
+
+    this.socket = null;
+    this.random = new Random();
+    this.connectionExpiration = null;
+    this.stop = false;
+  }
+
+  @Override
+  protected void multiAnnounce(AnnounceRequestMessage.RequestEvent event, boolean inhibitEvent, List<? extends AnnounceableInformation> torrents, List<Peer> peer) throws AnnounceException {
+    throw new AnnounceException("Not implemented");
+  }
+
+  @Override
+  public void announce(final AnnounceRequestMessage.RequestEvent event,
+                       boolean inhibitEvents, final AnnounceableInformation torrent, final List<Peer> peers) throws AnnounceException {
+    logAnnounceRequest(event, torrent);
+
+    State state = State.CONNECT_REQUEST;
+    int maxAttempts = AnnounceRequestMessage.RequestEvent
+            .STOPPED.equals(event)
+            ? UDP_MAX_TRIES_ON_STOPPED
+            : UDP_MAX_TRIES;
+    int attempts = -1;
+
+    try {
+      this.socket = new DatagramSocket();
+      this.socket.connect(this.address);
+
+      while (++attempts <= maxAttempts) {
+        // Transaction ID is randomized for each exchange.
+        this.transactionId = this.random.nextInt();
+
+        // Immediately decide if we can send the announce request
+        // directly or not. For this, we need a valid, non-expired
+        // connection ID.
+        if (this.connectionExpiration != null) {
+          if (new Date().before(this.connectionExpiration)) {
+            state = State.ANNOUNCE_REQUEST;
+          } else {
+            logger.debug("Announce connection ID expired, " +
+                    "reconnecting with tracker...");
+          }
+        }
+
+        switch (state) {
+          case CONNECT_REQUEST:
+            this.send(UDPConnectRequestMessage
+                    .craft(this.transactionId).getData());
+
+            try {
+              this.handleTrackerConnectResponse(
+                      UDPTrackerMessage.UDPTrackerResponseMessage
+                              .parse(this.recv(attempts)));
+              attempts = -1;
+            } catch (SocketTimeoutException ste) {
+              // Silently ignore the timeout and retry with a
+              // longer timeout, unless announce stop was
+              // requested in which case we need to exit right
+              // away.
+              if (stop) {
+                return;
+              }
+            }
+            break;
+
+          case ANNOUNCE_REQUEST:
+            for (Peer peer : peers) {
+              this.send(this.buildAnnounceRequest(event, torrent, peer).getData());
+            }
+
+            try {
+              this.handleTrackerAnnounceResponse(
+                      UDPTrackerMessage.UDPTrackerResponseMessage
+                              .parse(this.recv(attempts)), inhibitEvents, torrent.getHexInfoHash());
+              // If we got here, we succesfully completed this
+              // announce exchange and can simply return to exit the
+              // loop.
+              return;
+            } catch (SocketTimeoutException ste) {
+              // Silently ignore the timeout and retry with a
+              // longer timeout, unless announce stop was
+              // requested in which case we need to exit right
+              // away.
+              if (stop) {
+                return;
+              }
+            }
+            break;
+          default:
+            throw new IllegalStateException("Invalid announce state!");
+        }
+      }
+
+      // When the maximum number of attempts was reached, the announce
+      // really timed-out. We'll try again in the next announce loop.
+      throw new AnnounceException("Timeout while announcing" +
+              this.formatAnnounceEvent(event) + " to tracker!");
+    } catch (IOException ioe) {
+      throw new AnnounceException("Error while announcing" +
+              this.formatAnnounceEvent(event) +
+              " to tracker: " + ioe.getMessage(), ioe);
+    } catch (MessageValidationException mve) {
+      throw new AnnounceException("Tracker message violates expected " +
+              "protocol (" + mve.getMessage() + ")", mve);
+    }
+  }
+
+  /**
+   * Handles the tracker announce response message.
+   *
+   * <p>
+   * Verifies the transaction ID of the message before passing it over to
+   * {@link Announce#()}.
+   * </p>
+   *
+   * @param message The message received from the tracker in response to the
+   *                announce request.
+   */
+  @Override
+  protected void handleTrackerAnnounceResponse(TrackerMessage message,
+                                               boolean inhibitEvents, String hexInfoHash) throws AnnounceException {
+    this.validateTrackerResponse(message);
+    super.handleTrackerAnnounceResponse(message, inhibitEvents, hexInfoHash);
+  }
+
+  /**
+   * Close this announce connection.
+   */
+  @Override
+  protected void close() {
+    this.stop = true;
+
+    // Close the socket to force blocking operations to return.
+    if (this.socket != null && !this.socket.isClosed()) {
+      this.socket.close();
+    }
+  }
+
+  private UDPAnnounceRequestMessage buildAnnounceRequest(
+          final AnnounceRequestMessage.RequestEvent event, final AnnounceableInformation torrent, final Peer peer) {
+    return UDPAnnounceRequestMessage.craft(
+            this.connectionId,
+            transactionId,
+            torrent.getInfoHash(),
+            peer.getPeerIdArray(),
+            torrent.getDownloaded(),
+            torrent.getUploaded(),
+            torrent.getLeft(),
+            event,
+            peer.getAddress().getAddress(),
+            0,
+            AnnounceRequestMessage.DEFAULT_NUM_WANT,
+            peer.getPort());
+  }
+
+  /**
+   * Validates an incoming tracker message.
+   *
+   * <p>
+   * Verifies that the message is not an error message (throws an exception
+   * with the error message if it is) and that the transaction ID matches the
+   * current one.
+   * </p>
+   *
+   * @param message The incoming tracker message.
+   */
+  private void validateTrackerResponse(TrackerMessage message)
+          throws AnnounceException {
+    if (message instanceof ErrorMessage) {
+      throw new AnnounceException(((ErrorMessage) message).getReason());
+    }
+
+    if (message instanceof UDPTrackerMessage &&
+            (((UDPTrackerMessage) message).getTransactionId() != this.transactionId)) {
+      throw new AnnounceException("Invalid transaction ID!");
+    }
+  }
+
+  /**
+   * Handles the tracker connect response message.
+   *
+   * @param message The message received from the tracker in response to the
+   *                connection request.
+   */
+  private void handleTrackerConnectResponse(TrackerMessage message)
+          throws AnnounceException {
+    this.validateTrackerResponse(message);
+
+    if (!(message instanceof ConnectionResponseMessage)) {
+      throw new AnnounceException("Unexpected tracker message type " +
+              message.getType().name() + "!");
+    }
+
+    UDPConnectResponseMessage connectResponse =
+            (UDPConnectResponseMessage) message;
+
+    this.connectionId = connectResponse.getConnectionId();
+    Calendar now = Calendar.getInstance();
+    now.add(Calendar.MINUTE, 1);
+    this.connectionExpiration = now.getTime();
+  }
+
+  /**
+   * Send a UDP packet to the tracker.
+   *
+   * @param data The {@link ByteBuffer} to send in a datagram packet to the
+   *             tracker.
+   */
+  private void send(ByteBuffer data) {
+    try {
+      this.socket.send(new DatagramPacket(
+              data.array(),
+              data.capacity(),
+              this.address));
+    } catch (IOException ioe) {
+      logger.info("Error sending datagram packet to tracker at {}: {}.", this.address, ioe.getMessage());
+    }
+  }
+
+  /**
+   * Receive a UDP packet from the tracker.
+   *
+   * @param attempt The attempt number, used to calculate the timeout for the
+   *                receive operation.
+   * @retun Returns a {@link ByteBuffer} containing the packet data.
+   */
+  private ByteBuffer recv(int attempt)
+          throws IOException, SocketException, SocketTimeoutException {
+    int timeout = UDP_BASE_TIMEOUT_SECONDS * (int) Math.pow(2, attempt);
+    logger.trace("Setting receive timeout to {}s for attempt {}...",
+            timeout, attempt);
+    this.socket.setSoTimeout(timeout * 1000);
+
+    try {
+      DatagramPacket p = new DatagramPacket(
+              new byte[UDP_PACKET_LENGTH],
+              UDP_PACKET_LENGTH);
+      this.socket.receive(p);
+      return ByteBuffer.wrap(p.getData(), 0, p.getLength());
+    } catch (SocketTimeoutException ste) {
+      throw ste;
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/CountLimitConnectionAllower.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/CountLimitConnectionAllower.java
new file mode 100644
index 0000000..d3aee8b
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/CountLimitConnectionAllower.java
@@ -0,0 +1,35 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.client.PeersStorage;
+import com.turn.ttorrent.network.NewConnectionAllower;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.turn.ttorrent.Constants.DEFAULT_MAX_CONNECTION_COUNT;
+
+/**
+ * this implementation allows fixed count of open connection simultaneously
+ */
+
+//限制同时建立连接的数量
+public class CountLimitConnectionAllower implements NewConnectionAllower {
+
+  private final PeersStorage myPeersStorage;
+
+  private final AtomicInteger myMaxConnectionCount = new AtomicInteger();
+
+  public CountLimitConnectionAllower(PeersStorage peersStorage) {
+    this.myPeersStorage = peersStorage;
+    myMaxConnectionCount.set(DEFAULT_MAX_CONNECTION_COUNT);
+
+  }
+
+  public void setMyMaxConnectionCount(int newMaxCount) {
+    myMaxConnectionCount.set(newMaxCount);
+  }
+
+  @Override
+  public boolean isNewConnectionAllowed() {
+    return myPeersStorage.getSharingPeers().size() < myMaxConnectionCount.get();
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/DataProcessor.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/DataProcessor.java
new file mode 100644
index 0000000..eb9645a
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/DataProcessor.java
@@ -0,0 +1,27 @@
+package com.turn.ttorrent.client.network;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+
+public interface DataProcessor {
+
+  /**
+   * the method must read data from channel and process it
+   *
+   * @param socketChannel specified socket channel with data
+   * @return data processor which must process next data
+   * @throws IOException if an I/O error occurs
+   */
+  DataProcessor processAndGetNext(ByteChannel socketChannel) throws IOException;
+
+  /**
+   * the method must handle error and correctly release resources
+   *
+   * @param socketChannel specified channel
+   * @param e             specified exception
+   * @return data processor which must process next error. Can be null
+   * @throws IOException if an I/O error occurs
+   */
+  DataProcessor handleError(ByteChannel socketChannel, Throwable e) throws IOException;
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/DataProcessorUtil.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/DataProcessorUtil.java
new file mode 100644
index 0000000..f3a6399
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/DataProcessorUtil.java
@@ -0,0 +1,21 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.common.LoggerUtils;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+
+public final class DataProcessorUtil {
+
+  public static void closeChannelIfOpen(Logger logger, ByteChannel channel) {
+    if (channel.isOpen()) {
+      logger.trace("close channel {}", channel);
+      try {
+        channel.close();
+      } catch (IOException e) {
+        LoggerUtils.errorAndDebugDetails(logger, "unable to close channel {}", channel, e);
+      }
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/HandshakeReceiver.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/HandshakeReceiver.java
new file mode 100644
index 0000000..f3feb79
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/HandshakeReceiver.java
@@ -0,0 +1,171 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.client.Context;
+import com.turn.ttorrent.client.Handshake;
+import com.turn.ttorrent.client.LoadedTorrent;
+import com.turn.ttorrent.client.SharedTorrent;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.PeerUID;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.concurrent.RejectedExecutionException;
+
+public class HandshakeReceiver implements DataProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(HandshakeReceiver.class);
+
+  private final Context myContext;
+  private final String myHostAddress;
+  private final int myPort;
+  private final boolean myIsOutgoingConnection;
+  private ByteBuffer messageBytes;
+  private int pstrLength;
+
+  HandshakeReceiver(Context context,
+                    String hostAddress,
+                    int port,
+                    boolean isOutgoingListener) {
+    myContext = context;
+    myHostAddress = hostAddress;
+    myPort = port;
+    this.pstrLength = -1;
+    this.myIsOutgoingConnection = isOutgoingListener;
+  }
+
+  @Override
+  public DataProcessor processAndGetNext(ByteChannel socketChannel) throws IOException {
+
+    if (pstrLength == -1) {
+      ByteBuffer len = ByteBuffer.allocate(1);
+      int readBytes = -1;
+      try {
+        readBytes = socketChannel.read(len);
+      } catch (IOException ignored) {
+      }
+      if (readBytes == -1) {
+        return new ShutdownProcessor().processAndGetNext(socketChannel);
+      }
+      if (readBytes == 0) {
+        return this;
+      }
+      len.rewind();
+      byte pstrLen = len.get();
+      this.pstrLength = pstrLen;
+      messageBytes = ByteBuffer.allocate(this.pstrLength + Handshake.BASE_HANDSHAKE_LENGTH);
+      messageBytes.put(pstrLen);
+    }
+    int readBytes = -1;
+    try {
+      readBytes = socketChannel.read(messageBytes);
+    } catch (IOException e) {
+      LoggerUtils.warnAndDebugDetails(logger, "unable to read data from {}", socketChannel, e);
+    }
+    if (readBytes == -1) {
+      return new ShutdownProcessor().processAndGetNext(socketChannel);
+    }
+    if (messageBytes.remaining() != 0) {
+      return this;
+    }
+    Handshake hs = parseHandshake(socketChannel.toString());
+
+    if (hs == null) {
+      return new ShutdownProcessor().processAndGetNext(socketChannel);
+    }
+
+    final LoadedTorrent announceableTorrent = myContext.getTorrentsStorage().getLoadedTorrent(hs.getHexInfoHash());
+
+    if (announceableTorrent == null) {
+      logger.debug("Announceable torrent {} is not found in storage", hs.getHexInfoHash());
+      return new ShutdownProcessor().processAndGetNext(socketChannel);
+    }
+
+    SharedTorrent torrent;
+    try {
+      torrent = myContext.getTorrentLoader().loadTorrent(announceableTorrent);
+    } catch (IllegalStateException e) {
+      return new ShutdownProcessor().processAndGetNext(socketChannel);
+    } catch(Exception e) {
+      LoggerUtils.warnWithMessageAndDebugDetails(logger, "cannot load torrent {}", hs.getHexInfoHash(), e);
+      return new ShutdownProcessor().processAndGetNext(socketChannel);
+    }
+
+    logger.trace("got handshake {} from {}", Arrays.toString(messageBytes.array()), socketChannel);
+
+    String clientTypeVersion = new String(Arrays.copyOf(hs.getPeerId(), 8));
+    String clientType = clientTypeVersion.substring(1, 3);
+    int clientVersion = 0;
+    try {
+      clientVersion = Integer.parseInt(clientTypeVersion.substring(3, 7));
+    } catch (NumberFormatException ignored) {}
+    final SharingPeer sharingPeer =
+            myContext.createSharingPeer(myHostAddress,
+                    myPort,
+                    ByteBuffer.wrap(hs.getPeerId()),
+                    torrent,
+                    socketChannel,
+                    clientType,
+                    clientVersion);
+    PeerUID peerUID = new PeerUID(sharingPeer.getAddress(), hs.getHexInfoHash());
+
+    SharingPeer old = myContext.getPeersStorage().putIfAbsent(peerUID, sharingPeer);
+    if (old != null) {
+      logger.debug("Already connected to old peer {}, close current connection with {}", old, sharingPeer);
+      return new ShutdownProcessor().processAndGetNext(socketChannel);
+    }
+
+    // If I am not a leecher
+    if (!myIsOutgoingConnection) {
+      logger.trace("send handshake to {}", socketChannel);
+      try {
+        final Handshake craft = Handshake.craft(hs.getInfoHash(), myContext.getPeersStorage().getSelf().getPeerIdArray());
+        socketChannel.write(craft.getData());
+      } catch (IOException e) {
+        LoggerUtils.warnAndDebugDetails(logger, "error in sending handshake to {}", socketChannel, e);
+        return new ShutdownAndRemovePeerProcessor(peerUID, myContext);
+      }
+    }
+
+    logger.debug("setup new connection with {}", sharingPeer);
+
+    try {
+      myContext.getExecutor().submit(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            sharingPeer.onConnectionEstablished();
+          } catch (Throwable e) {
+            LoggerUtils.warnAndDebugDetails(logger, "unhandled exception {} in executor task (onConnectionEstablished)", e.toString(), e);
+          }
+        }
+      });
+      torrent.addConnectedPeer(sharingPeer);
+    } catch (RejectedExecutionException e) {
+      LoggerUtils.warnAndDebugDetails(logger, "task 'onConnectionEstablished' submit is failed. Reason: {}", e.getMessage(), e);
+      return new ShutdownAndRemovePeerProcessor(peerUID, myContext).processAndGetNext(socketChannel);
+    }
+
+    return new WorkingReceiver(peerUID, myContext);
+  }
+
+  private Handshake parseHandshake(String socketChannelForLog) throws IOException {
+    try {
+      messageBytes.rewind();
+      return Handshake.parse(messageBytes, pstrLength);
+    } catch (ParseException e) {
+      logger.info("incorrect handshake message from " + socketChannelForLog, e);
+    }
+    return null;
+  }
+
+  @Override
+  public DataProcessor handleError(ByteChannel socketChannel, Throwable e) throws IOException {
+    return new ShutdownProcessor().processAndGetNext(socketChannel);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/HandshakeSender.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/HandshakeSender.java
new file mode 100644
index 0000000..9040efb
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/HandshakeSender.java
@@ -0,0 +1,61 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.client.Context;
+import com.turn.ttorrent.client.Handshake;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentHash;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.util.Arrays;
+
+public class HandshakeSender implements DataProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(HandshakeSender.class);
+
+  private final TorrentHash myTorrentHash;
+  private final String myRemotePeerIp;
+  private final int myRemotePeerPort;
+  private final Context myContext;
+
+  public HandshakeSender(TorrentHash torrentHash,
+                         String remotePeerIp,
+                         int remotePeerPort,
+                         Context context) {
+    myTorrentHash = torrentHash;
+    myRemotePeerIp = remotePeerIp;
+    myRemotePeerPort = remotePeerPort;
+    myContext = context;
+  }
+
+  @Override
+  public DataProcessor processAndGetNext(ByteChannel socketChannel) throws IOException {
+
+    Peer self = myContext.getPeersStorage().getSelf();
+    Handshake handshake = Handshake.craft(myTorrentHash.getInfoHash(), self.getPeerIdArray());
+    if (handshake == null) {
+      logger.warn("can not craft handshake message. Self peer id is {}, torrent hash is {}",
+              Arrays.toString(self.getPeerIdArray()),
+              Arrays.toString(myTorrentHash.getInfoHash()));
+      return new ShutdownProcessor();
+    }
+    ByteBuffer messageToSend = ByteBuffer.wrap(handshake.getData().array());
+    logger.trace("try send handshake {} to {}", handshake, socketChannel);
+    while (messageToSend.hasRemaining()) {
+      socketChannel.write(messageToSend);
+    }
+    return new HandshakeReceiver(
+            myContext,
+            myRemotePeerIp,
+            myRemotePeerPort,
+            true);
+  }
+
+  @Override
+  public DataProcessor handleError(ByteChannel socketChannel, Throwable e) throws IOException {
+    return new ShutdownProcessor().processAndGetNext(socketChannel);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/OutgoingConnectionListener.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/OutgoingConnectionListener.java
new file mode 100644
index 0000000..d0cd4c6
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/OutgoingConnectionListener.java
@@ -0,0 +1,48 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.client.Context;
+import com.turn.ttorrent.common.TorrentHash;
+import com.turn.ttorrent.network.ConnectionListener;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+
+public class OutgoingConnectionListener implements ConnectionListener {
+
+  private volatile DataProcessor myNext;
+  private final TorrentHash torrentHash;
+  private final String myRemotePeerIp;
+  private final int myRemotePeerPort;
+  private final Context myContext;
+
+  public OutgoingConnectionListener(Context context,
+                                    TorrentHash torrentHash,
+                                    String remotePeerIp,
+                                    int remotePeerPort) {
+    this.torrentHash = torrentHash;
+    myRemotePeerIp = remotePeerIp;
+    myRemotePeerPort = remotePeerPort;
+    myNext = new ShutdownProcessor();
+    myContext = context;
+  }
+
+  @Override
+  public void onNewDataAvailable(SocketChannel socketChannel) throws IOException {
+    this.myNext = this.myNext.processAndGetNext(socketChannel);
+  }
+
+  @Override
+  public void onConnectionEstablished(SocketChannel socketChannel) throws IOException {
+    HandshakeSender handshakeSender = new HandshakeSender(
+            torrentHash,
+            myRemotePeerIp,
+            myRemotePeerPort,
+            myContext);
+    this.myNext = handshakeSender.processAndGetNext(socketChannel);
+  }
+
+  @Override
+  public void onError(SocketChannel socketChannel, Throwable ex) throws IOException {
+    this.myNext.handleError(socketChannel, ex);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/ShutdownAndRemovePeerProcessor.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/ShutdownAndRemovePeerProcessor.java
new file mode 100644
index 0000000..2db3676
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/ShutdownAndRemovePeerProcessor.java
@@ -0,0 +1,47 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.client.Context;
+import com.turn.ttorrent.client.PeersStorage;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.common.PeerUID;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+
+public class ShutdownAndRemovePeerProcessor implements DataProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(ShutdownAndRemovePeerProcessor.class);
+
+  private final PeerUID myPeerUID;
+  private final Context myContext;
+
+  public ShutdownAndRemovePeerProcessor(PeerUID peerId, Context context) {
+    myPeerUID = peerId;
+    myContext = context;
+  }
+
+  @Override
+  public DataProcessor processAndGetNext(ByteChannel socketChannel) throws IOException {
+    DataProcessorUtil.closeChannelIfOpen(logger, socketChannel);
+    logger.trace("try remove and unbind peer. Peer UID - {}", myPeerUID);
+    removePeer();
+    return null;
+  }
+
+  private void removePeer() {
+    PeersStorage peersStorage = myContext.getPeersStorage();
+    SharingPeer removedPeer = peersStorage.removeSharingPeer(myPeerUID);
+    if (removedPeer == null) {
+      logger.info("try to shutdown peer with id {}, but it is not found in storage", myPeerUID);
+      return;
+    }
+    removedPeer.unbind(true);
+  }
+
+  @Override
+  public DataProcessor handleError(ByteChannel socketChannel, Throwable e) throws IOException {
+    return processAndGetNext(socketChannel);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/ShutdownProcessor.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/ShutdownProcessor.java
new file mode 100644
index 0000000..b16a883
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/ShutdownProcessor.java
@@ -0,0 +1,23 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.channels.ByteChannel;
+
+public class ShutdownProcessor implements DataProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(ShutdownProcessor.class);
+
+  @Override
+  public DataProcessor processAndGetNext(ByteChannel socketChannel) throws IOException {
+    DataProcessorUtil.closeChannelIfOpen(logger, socketChannel);
+    return null;
+  }
+
+  @Override
+  public DataProcessor handleError(ByteChannel socketChannel, Throwable e) throws IOException {
+    return processAndGetNext(socketChannel);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/StateChannelListener.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/StateChannelListener.java
new file mode 100644
index 0000000..cde66f1
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/StateChannelListener.java
@@ -0,0 +1,37 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.client.Context;
+import com.turn.ttorrent.network.ConnectionListener;
+
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+
+public class StateChannelListener implements ConnectionListener {
+
+  private volatile DataProcessor myNext;
+  private final Context myContext;
+
+  public StateChannelListener(Context context) {
+    myContext = context;
+    myNext = new ShutdownProcessor();
+  }
+
+  @Override
+  public void onNewDataAvailable(SocketChannel socketChannel) throws IOException {
+    this.myNext = this.myNext.processAndGetNext(socketChannel);
+  }
+
+  @Override
+  public void onConnectionEstablished(SocketChannel socketChannel) throws IOException {
+    this.myNext = new HandshakeReceiver(
+            myContext,
+            socketChannel.socket().getInetAddress().getHostAddress(),
+            socketChannel.socket().getPort(),
+            false);
+  }
+
+  @Override
+  public void onError(SocketChannel socketChannel, Throwable ex) throws IOException {
+    this.myNext = this.myNext.handleError(socketChannel, ex);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/WorkingReceiver.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/WorkingReceiver.java
new file mode 100644
index 0000000..ba46de5
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/network/WorkingReceiver.java
@@ -0,0 +1,156 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.client.Context;
+import com.turn.ttorrent.client.SharedTorrent;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.PeerUID;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.protocol.PeerMessage;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.concurrent.RejectedExecutionException;
+
+public class WorkingReceiver implements DataProcessor {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(WorkingReceiver.class);
+  //16 bytes is sufficient for all torrents messages except bitfield and piece.
+  //So piece and bitfield have dynamic size because bytebuffer for this messages will be allocated after get message length
+  private static final int DEF_BUFFER_SIZE = 16;
+  private static final int MAX_MESSAGE_SIZE = 2 * 1024 * 1024;
+
+  private final PeerUID myPeerUID;
+  private final Context myContext;
+  @NotNull
+  private ByteBuffer messageBytes;
+  private int pstrLength;
+
+  WorkingReceiver(PeerUID peerId,
+                         Context context) {
+    myPeerUID = peerId;
+    myContext = context;
+
+    this.messageBytes = ByteBuffer.allocate(DEF_BUFFER_SIZE);
+    this.pstrLength = -1;
+  }
+
+  @Override
+  public DataProcessor processAndGetNext(ByteChannel socketChannel) throws IOException {
+    logger.trace("received data from channel", socketChannel);
+    if (pstrLength == -1) {
+      messageBytes.limit(PeerMessage.MESSAGE_LENGTH_FIELD_SIZE);
+      final int read;
+      try {
+        read = socketChannel.read(messageBytes);
+      } catch (IOException e) {
+        //Some clients close connection so that java throws IOException "An existing connection was forcibly closed by the remote host"
+        logger.debug("unable to read data from channel " + socketChannel, e);
+        return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+      }
+      if (read < 0) {
+        logger.debug("channel {} is closed by other peer", socketChannel);
+        return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+      }
+      if (messageBytes.hasRemaining()) {
+        return this;
+      }
+      this.pstrLength = messageBytes.getInt(0);
+      logger.trace("read of message length finished, Message length is {}", this.pstrLength);
+
+      if (this.pstrLength > MAX_MESSAGE_SIZE) {
+        logger.warn("Proposed limit of {} is larger than max message size {}",
+                PeerMessage.MESSAGE_LENGTH_FIELD_SIZE + this.pstrLength, MAX_MESSAGE_SIZE);
+        logger.warn("current bytes in buffer is {}", Arrays.toString(messageBytes.array()));
+        logger.warn("Close connection with peer {}", myPeerUID);
+        return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+      }
+    }
+
+    if (PeerMessage.MESSAGE_LENGTH_FIELD_SIZE + this.pstrLength > messageBytes.capacity()) {
+      ByteBuffer old = messageBytes;
+      old.rewind();
+      messageBytes = ByteBuffer.allocate(PeerMessage.MESSAGE_LENGTH_FIELD_SIZE + this.pstrLength);
+      messageBytes.put(old);
+    }
+
+    messageBytes.limit(PeerMessage.MESSAGE_LENGTH_FIELD_SIZE + this.pstrLength);
+
+    logger.trace("try read data from {}", socketChannel);
+    int readBytes;
+    try {
+      readBytes = socketChannel.read(messageBytes);
+    } catch (IOException e) {
+      return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+    }
+    if (readBytes < 0) {
+      logger.debug("channel {} is closed by other peer", socketChannel);
+      return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+    }
+    if (messageBytes.hasRemaining()) {
+      logger.trace("buffer is not full, continue reading...");
+      return this;
+    }
+    logger.trace("finished read data from {}", socketChannel);
+
+    messageBytes.rewind();
+    this.pstrLength = -1;
+
+    final SharingPeer peer = myContext.getPeersStorage().getSharingPeer(myPeerUID);
+
+    final String hexInfoHash = peer.getHexInfoHash();
+    SharedTorrent torrent = myContext.getTorrentsStorage().getTorrent(hexInfoHash);
+    if (torrent == null || !myContext.getTorrentsStorage().hasTorrent(hexInfoHash)) {
+      logger.debug("torrent with hash {} for peer {} doesn't found in storage. Maybe somebody deletes it manually", hexInfoHash, peer);
+      return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+    }
+
+    logger.trace("try parse message from {}. Torrent {}", peer, torrent);
+    ByteBuffer bufferCopy = ByteBuffer.wrap(Arrays.copyOf(messageBytes.array(), messageBytes.limit()));
+
+    this.messageBytes = ByteBuffer.allocate(DEF_BUFFER_SIZE);
+    final PeerMessage message;
+
+    try {
+      message = PeerMessage.parse(bufferCopy, torrent);
+    } catch (ParseException e) {
+      LoggerUtils.warnAndDebugDetails(logger, "incorrect message was received from peer {}", peer, e);
+      return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+    }
+
+    logger.trace("get message {} from {}", message, socketChannel);
+
+    try {
+      myContext.getExecutor().submit(new Runnable() {
+        @Override
+        public void run() {
+          final Thread currentThread = Thread.currentThread();
+          final String oldName = currentThread.getName();
+          try {
+            currentThread.setName(oldName + " handle message for torrent " + myPeerUID.getTorrentHash() + " peer: " + peer.getHostIdentifier());
+            peer.handleMessage(message);
+          } catch (Throwable e) {
+            LoggerUtils.warnAndDebugDetails(logger, "unhandled exception {} in executor task (handleMessage)", e.toString(), e);
+          } finally {
+            currentThread.setName(oldName);
+          }
+
+        }
+      });
+    } catch (RejectedExecutionException e) {
+      LoggerUtils.warnAndDebugDetails(logger, "task submit is failed. Reason: {}", e.getMessage(), e);
+      return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+    }
+    return this;
+  }
+
+  @Override
+  public DataProcessor handleError(ByteChannel socketChannel, Throwable e) throws IOException {
+    return new ShutdownAndRemovePeerProcessor(myPeerUID, myContext).processAndGetNext(socketChannel);
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/MessageListener.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/MessageListener.java
new file mode 100644
index 0000000..898ca33
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/MessageListener.java
@@ -0,0 +1,32 @@
+/**
+ * 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.client.peer;
+
+import com.turn.ttorrent.common.protocol.PeerMessage;
+
+import java.util.EventListener;
+
+
+/**
+ * EventListener interface for objects that want to receive incoming messages
+ * from peers.
+ *
+ * @author mpetazzoni
+ */
+public interface MessageListener extends EventListener {
+
+  void handleMessage(PeerMessage msg);
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java
new file mode 100644
index 0000000..786e889
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/PeerActivityListener.java
@@ -0,0 +1,142 @@
+/**
+ * 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.client.peer;
+
+import com.turn.ttorrent.client.Piece;
+
+import java.io.IOException;
+import java.util.BitSet;
+import java.util.EventListener;
+
+
+/**
+ * EventListener interface for objects that want to handle peer activity
+ * events like piece availability, or piece completion events, and more.
+ *
+ * @author mpetazzoni
+ */
+public interface PeerActivityListener extends EventListener {
+
+  /**
+   * Peer choked handler.
+   *
+   * <p>
+   * This handler is fired when a peer choked and now refuses to send data to
+   * us. This means we should not try to request or expect anything from it
+   * until it becomes ready again.
+   * </p>
+   *
+   * @param peer The peer that choked.
+   */
+  void handlePeerChoked(SharingPeer peer);
+
+  /**
+   * Peer ready handler.
+   *
+   * <p>
+   * This handler is fired when a peer notified that it is no longer choked.
+   * This means we can send piece block requests to it and start downloading.
+   * </p>
+   *
+   * @param peer The peer that became ready.
+   */
+  void handlePeerReady(SharingPeer peer);
+
+  /**
+   * Piece availability handler.
+   *
+   * <p>
+   * This handler is fired when an update in piece availability is received
+   * from a peer's HAVE message.
+   * </p>
+   *
+   * @param peer  The peer we got the update from.
+   * @param piece The piece that became available from this peer.
+   */
+  void handlePieceAvailability(SharingPeer peer, Piece piece);
+
+  /**
+   * Bit field availability handler.
+   *
+   * <p>
+   * This handler is fired when an update in piece availability is received
+   * from a peer's BITFIELD message.
+   * </p>
+   *
+   * @param peer            The peer we got the update from.
+   * @param availablePieces The pieces availability bit field of the peer.
+   */
+  void handleBitfieldAvailability(SharingPeer peer,
+                                  BitSet availablePieces);
+
+  /**
+   * Piece upload completion handler.
+   *
+   * <p>
+   * This handler is fired when a piece has been uploaded entirely to a peer.
+   * </p>
+   *
+   * @param peer  The peer the piece was sent to.
+   * @param piece The piece in question.
+   */
+  void handlePieceSent(SharingPeer peer, Piece piece);
+
+  /**
+   * Piece download completion handler.
+   *
+   * <p>
+   * This handler is fired when a piece has been downloaded entirely and the
+   * piece data has been revalidated.
+   * </p>
+   *
+   * <p>
+   * <b>Note:</b> the piece may <em>not</em> be valid after it has been
+   * downloaded, in which case appropriate action should be taken to
+   * redownload the piece.
+   * </p>
+   *
+   * @param peer  The peer we got this piece from.
+   * @param piece The piece in question.
+   */
+  void handlePieceCompleted(SharingPeer peer, Piece piece)
+          throws IOException;
+
+  /**
+   * Peer disconnection handler.
+   *
+   * <p>
+   * This handler is fired when a peer disconnects, or is disconnected due to
+   * protocol violation.
+   * </p>
+   *
+   * @param peer The peer we got this piece from.
+   */
+  void handlePeerDisconnected(SharingPeer peer);
+
+  /**
+   * Handler for IOException during peer operation.
+   *
+   * @param peer The peer whose activity trigger the exception.
+   * @param ioe  The IOException object, for reporting.
+   */
+  void handleIOException(SharingPeer peer, IOException ioe);
+
+
+  void handleNewPeerConnected(SharingPeer peer);
+
+  void afterPeerRemoved(SharingPeer peer);
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Rate.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Rate.java
new file mode 100644
index 0000000..9db88f3
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/Rate.java
@@ -0,0 +1,128 @@
+/**
+ * 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.client.peer;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+
+/**
+ * A data exchange rate representation.
+ *
+ * <p>
+ * This is a utility class to keep track, and compare, of the data exchange
+ * rate (either download or upload) with a peer.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public class Rate implements Comparable<Rate> {
+
+  public static final Comparator<Rate> RATE_COMPARATOR =
+          new RateComparator();
+
+  private long bytes = 0;
+  private long reset = 0;
+  private long last = 0;
+
+  /**
+   * Add a byte count to the current measurement.
+   *
+   * @param count The number of bytes exchanged since the last reset.
+   */
+  public synchronized void add(long count) {
+    this.bytes += count;
+    if (this.reset == 0) {
+      this.reset = System.currentTimeMillis();
+    }
+    this.last = System.currentTimeMillis();
+  }
+
+  /**
+   * Get the current rate.
+   *
+   * <p>
+   * The exchange rate is the number of bytes exchanged since the last
+   * reset and the last input.
+   * </p>
+   */
+  public synchronized float get() {
+    if (this.last - this.reset == 0) {
+      return 0;
+    }
+
+    return this.bytes / ((this.last - this.reset) / 1000.0f);
+  }
+
+  /**
+   * Reset the measurement.
+   */
+  public synchronized void reset() {
+    this.bytes = 0;
+    this.reset = System.currentTimeMillis();
+    this.last = this.reset;
+  }
+
+  @Override
+  public int compareTo(Rate other) {
+    return RATE_COMPARATOR.compare(this, other);
+  }
+
+  /**
+   * A rate comparator.
+   *
+   * <p>
+   * This class provides a comparator to sort peers by an exchange rate,
+   * comparing two rates and returning an ascending ordering.
+   * </p>
+   *
+   * <p>
+   * <b>Note:</b> we need to make sure here that we don't return 0, which
+   * would provide an ordering that is inconsistent with
+   * <code>equals()</code>'s behavior, and result in unpredictable behavior
+   * for sorted collections using this comparator.
+   * </p>
+   *
+   * @author mpetazzoni
+   */
+  private static class RateComparator
+          implements Comparator<Rate>, Serializable {
+
+    private static final long serialVersionUID = 72460233003600L;
+
+    /**
+     * Compare two rates together.
+     *
+     * <p>
+     * This method compares float, but we don't care too much about
+     * rounding errors. It's just to order peers so super-strict rate based
+     * order is not required.
+     * </p>
+     *
+     * @param a
+     * @param b
+     */
+    @Override
+    public int compare(Rate a, Rate b) {
+      if (a.get() > b.get()) {
+        return 1;
+      }
+
+      return -1;
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/SharingPeer.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/SharingPeer.java
new file mode 100644
index 0000000..1cb3b34
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/SharingPeer.java
@@ -0,0 +1,807 @@
+/**
+ * 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.client.peer;
+
+import com.turn.ttorrent.client.PeerInformation;
+import com.turn.ttorrent.client.Piece;
+import com.turn.ttorrent.client.SharedTorrent;
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.TorrentUtils;
+import com.turn.ttorrent.common.protocol.PeerMessage;
+import com.turn.ttorrent.network.ConnectionClosedException;
+import com.turn.ttorrent.network.ConnectionManager;
+import com.turn.ttorrent.network.WriteListener;
+import com.turn.ttorrent.network.WriteTask;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * A peer exchanging on a torrent wi th the BitTorrent client.
+ * 数据交换
+ * <p/>
+ * <p>
+ * A SharingPeer extends the base Peer class with all the data and logic needed
+ * by the BitTorrent client to interact with a peer exchanging on the same
+ * torrent.
+ * </p>
+ * <p/>
+ * <p>
+ * Peers are defined by their peer ID, IP address and port number, just like
+ * base peers. Peers we exchange with also contain four crucial attributes:
+ * </p>
+ * <p/>
+ * <ul>
+ * <li><code>choking</code>, which means we are choking this peer and we're
+ * not willing to send him anything for now;</li>
+ * <li><code>interesting</code>, which means we are interested in a piece
+ * this peer has;</li>
+ * <li><code>choked</code>, if this peer is choking and won't send us
+ * anything right now;</li>
+ * <li><code>interested</code>, if this peer is interested in something we
+ * have.</li>
+ * </ul>
+ * <p/>
+ * <p>
+ * Peers start choked and uninterested.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public class SharingPeer extends Peer implements MessageListener, PeerInformation {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(SharingPeer.class);
+
+  private final Object availablePiecesLock;
+  private volatile boolean choking;
+  private volatile boolean interesting;
+  private volatile boolean choked;
+  private volatile boolean interested;
+  private final SharedTorrent torrent;
+  private final BitSet availablePieces;
+  private BitSet poorlyAvailablePieces;
+  private final Map<Piece, Integer> myRequestedPieces;
+
+  private volatile boolean downloading;
+
+  private final Rate download;
+  private final Rate upload;
+  private final AtomicInteger downloadedPiecesCount;
+  private final List<PeerActivityListener> listeners;
+
+  private final Object requestsLock;
+
+  private final AtomicBoolean isStopped;
+
+  private final ConnectionManager connectionManager;
+  private final ByteChannel socketChannel;
+
+  private final String clientIdentifier;
+  private final int clientVersion;
+
+  /**
+   * Create a new sharing peer on a given torrent.
+   *  @param ip      The peer's IP address.
+   * @param port    The peer's port.
+   * @param peerId  The byte-encoded peer ID.
+   * @param torrent The torrent this peer exchanges with us on.
+   * @param clientIdentifier
+   * @param clientVersion
+   */
+  public SharingPeer(String ip,
+                     int port,
+                     ByteBuffer peerId,
+                     SharedTorrent torrent,
+                     ConnectionManager connectionManager,
+                     PeerActivityListener client,
+                     ByteChannel channel,
+                     String clientIdentifier,
+                     int clientVersion) {
+    super(ip, port, peerId);
+
+    this.torrent = torrent;
+    this.clientIdentifier = clientIdentifier;
+    this.clientVersion = clientVersion;
+    this.listeners = Arrays.asList(client, torrent);
+    this.availablePieces = new BitSet(torrent.getPieceCount());
+    this.poorlyAvailablePieces = new BitSet(torrent.getPieceCount());
+
+    this.requestsLock = new Object();
+    this.socketChannel = channel;
+    this.isStopped = new AtomicBoolean(false);
+    this.availablePiecesLock = new Object();
+    this.myRequestedPieces = new HashMap<Piece, Integer>();
+    this.connectionManager = connectionManager;
+    this.download = new Rate();
+    this.upload = new Rate();
+    this.setTorrentHash(torrent.getHexInfoHash());
+    this.choking = true;
+    this.interesting = false;
+    this.choked = true;
+    this.interested = false;
+    this.downloading = false;
+    this.downloadedPiecesCount = new AtomicInteger();
+  }
+
+  public Rate getDLRate() {
+    return this.download;
+  }
+
+  public Rate getULRate() {
+    return this.upload;
+  }
+
+  /**
+   * Choke this peer.
+   * <p/>
+   * <p>
+   * We don't want to upload to this peer anymore, so mark that we're choking
+   * from this peer.
+   * </p>
+   */
+  public void choke() {
+    if (!this.choking) {
+      logger.trace("Choking {}", this);
+      this.send(PeerMessage.ChokeMessage.craft());
+      this.choking = true;
+    }
+  }
+
+  @Override
+  public byte[] getId() {
+    return getPeerIdArray();
+  }
+
+  @Override
+  public String getClientIdentifier() {
+    return clientIdentifier;
+  }
+
+  @Override
+  public int getClientVersion() {
+    return clientVersion;
+  }
+
+  public void onConnectionEstablished() {
+    firePeerConnected();
+    BitSet pieces = this.torrent.getCompletedPieces();
+    if (pieces.cardinality() > 0) {
+      this.send(PeerMessage.BitfieldMessage.craft(pieces));
+    }
+    resetRates();
+  }
+
+  /**
+   * Unchoke this peer.
+   * <p/>
+   * <p>
+   * Mark that we are no longer choking from this peer and can resume
+   * uploading to it.
+   * </p>
+   */
+  public void unchoke() {
+    logger.trace("Unchoking {}", this);
+    this.send(PeerMessage.UnchokeMessage.craft());
+    this.choking = false;
+  }
+
+  public boolean isChoking() {
+    return this.choking;
+  }
+
+  public void interesting() {
+    if (!this.interesting) {
+      logger.trace("Telling {} we're interested.", this);
+      this.send(PeerMessage.InterestedMessage.craft());
+      this.interesting = true;
+    }
+  }
+
+  public void notInteresting() {
+    if (this.interesting) {
+      logger.trace("Telling {} we're no longer interested.", this);
+      this.send(PeerMessage.NotInterestedMessage.craft());
+      this.interesting = false;
+    }
+  }
+
+  public boolean isInteresting() {
+    return this.interesting;
+  }
+
+  public boolean isChoked() {
+    return this.choked;
+  }
+
+  public boolean isInterested() {
+    return this.interested;
+  }
+
+  public BitSet getPoorlyAvailablePieces() {
+    return poorlyAvailablePieces;
+  }
+
+  /**
+   * Returns the available pieces from this peer.
+   *
+   * @return A clone of the available pieces bit field from this peer.
+   */
+  public BitSet getAvailablePieces() {
+    synchronized (this.availablePiecesLock) {
+      return (BitSet) this.availablePieces.clone();
+    }
+  }
+
+  /**
+   * Returns the currently requested piece, if any.
+   */
+  public Set<Piece> getRequestedPieces() {
+    synchronized (requestsLock) {
+      return myRequestedPieces.keySet();
+    }
+  }
+
+  public void resetRates() {
+    this.download.reset();
+    this.upload.reset();
+  }
+
+  public void pieceDownloaded() {
+    downloadedPiecesCount.incrementAndGet();
+  }
+
+  public int getDownloadedPiecesCount() {
+    return downloadedPiecesCount.get();
+  }
+
+  /**
+   * Tells whether this peer as an active connection through a peer exchange.
+   */
+  public boolean isConnected() {
+    return this.socketChannel.isOpen();
+  }
+
+  /**
+   * Unbind and disconnect this peer.
+   * <p/>
+   * <p>
+   * This terminates the eventually present and/or connected peer exchange
+   * with the peer and fires the peer disconnected event to any peer activity
+   * listeners registered on this peer.
+   * </p>
+   *
+   * @param force Force unbind without sending cancel requests.
+   */
+  public void unbind(boolean force) {
+    if (isStopped.getAndSet(true))
+      return;
+
+    try {
+      connectionManager.closeChannel(socketChannel);
+    } catch (IOException e) {
+      LoggerUtils.errorAndDebugDetails(logger, "cannot close socket channel. Peer {}", this, e);
+    }
+
+    this.firePeerDisconnected();
+
+    synchronized (requestsLock) {
+      this.downloading = myRequestedPieces.size() > 0;
+      myRequestedPieces.clear();
+    }
+
+    this.afterPeerDisconnected();
+  }
+
+  /**
+   * Send a message to the peer.
+   * <p/>
+   * <p>
+   * Delivery of the message can only happen if the peer is connected.
+   * </p>
+   *
+   * @param message The message to send to the remote peer through our peer
+   *                exchange.
+   */
+  public void send(PeerMessage message) throws IllegalStateException {
+    logger.trace("Sending msg {} to {}", message.getType(), this);
+    if (this.isConnected()) {
+      ByteBuffer data = message.getData();
+      data.rewind();
+      connectionManager.offerWrite(new WriteTask(socketChannel, data, new WriteListener() {
+        @Override
+        public void onWriteFailed(String message, Throwable e) {
+          if (e == null) {
+            logger.info(message);
+          } else if (e instanceof ConnectionClosedException){
+            logger.debug(message, e);
+            unbind(true);
+          } else {
+            LoggerUtils.warnAndDebugDetails(logger, message, e);
+          }
+
+        }
+
+        @Override
+        public void onWriteDone() {
+        }
+      }), 1, TimeUnit.SECONDS);
+    } else {
+      logger.trace("Attempting to send a message to non-connected peer {}!", this);
+      unbind(true);
+    }
+  }
+
+  /**
+   * Download the given piece from this peer.
+   * <p/>
+   * <p>
+   * Starts a block request queue and pre-fill it with MAX_PIPELINED_REQUESTS
+   * block requests.
+   * </p>
+   * <p/>
+   * <p>
+   * Further requests will be added, one by one, every time a block is
+   * returned.
+   * </p>
+   *
+   * @param piece The piece chosen to be downloaded from this peer.
+   */
+  public void downloadPiece(final Piece piece)
+          throws IllegalStateException {
+    List<PeerMessage.RequestMessage> toSend = new ArrayList<PeerMessage.RequestMessage>();
+    synchronized (this.requestsLock) {
+      if (myRequestedPieces.containsKey(piece)) {
+        //already requested
+        return;
+      }
+      int requestedBlocksCount = 0;
+      int lastRequestedOffset = 0;
+      while (lastRequestedOffset < piece.size()) {
+        PeerMessage.RequestMessage request = PeerMessage.RequestMessage
+                .craft(piece.getIndex(), lastRequestedOffset,
+                        Math.min((int) (piece.size() - lastRequestedOffset),
+                                PeerMessage.RequestMessage.DEFAULT_REQUEST_SIZE));
+        toSend.add(request);
+        requestedBlocksCount++;
+        lastRequestedOffset = request.getLength() + lastRequestedOffset;
+      }
+      myRequestedPieces.put(piece, requestedBlocksCount);
+      this.downloading = myRequestedPieces.size() > 0;
+    }
+    for (PeerMessage.RequestMessage requestMessage : toSend) {
+      this.send(requestMessage);
+    }
+  }
+
+  public boolean isDownloading() {
+    return this.downloading;
+  }
+
+  /**
+   * Remove the REQUEST message from the request pipeline matching this
+   * PIECE message.
+   * <p/>
+   * <p>
+   * Upon reception of a piece block with a PIECE message, remove the
+   * corresponding request from the pipeline to make room for the next block
+   * requests.
+   * </p>
+   *
+   * @param piece The piece of PIECE message received.
+   */
+  private void removeBlockRequest(final Piece piece) {
+    synchronized (this.requestsLock) {
+      Integer requestedBlocksCount = myRequestedPieces.get(piece);
+      if (requestedBlocksCount == null) {
+        return;
+      }
+      if (requestedBlocksCount <= 1) {
+        //it's last block
+        myRequestedPieces.remove(piece);
+      } else {
+        myRequestedPieces.put(piece, requestedBlocksCount - 1);
+      }
+      this.downloading = myRequestedPieces.size() > 0;
+    }
+  }
+
+  /**
+   * Cancel all pending requests.
+   * <p/>
+   * <p>
+   * This queues CANCEL messages for all the requests in the queue, and
+   * returns the list of requests that were in the queue.
+   * </p>
+   * <p/>
+   * <p>
+   * If no request queue existed, or if it was empty, an empty set of request
+   * messages is returned.
+   * </p>
+   */
+  public void cancelPendingRequests() {
+    cancelPendingRequests(null);
+  }
+
+  public void cancelPendingRequests(@Nullable final Piece piece) {
+    synchronized (this.requestsLock) {
+      if (piece != null) {
+        myRequestedPieces.remove(piece);
+      } else {
+        myRequestedPieces.clear();
+      }
+      this.downloading = myRequestedPieces.size() > 0;
+    }
+  }
+
+  public int getRemainingRequestedPieces(final Piece piece) {
+    synchronized (this.requestsLock) {
+      Integer requestedBlocksCount = myRequestedPieces.get(piece);
+      if (requestedBlocksCount == null) return 0;
+      return requestedBlocksCount;
+    }
+  }
+
+  /**
+   * Handle an incoming message from this peer.
+   *
+   * @param msg The incoming, parsed message.
+   */
+  @Override
+  public void handleMessage(PeerMessage msg) {
+//    logger.trace("Received msg {} from {}", msg.getType(), this);
+    if (isStopped.get())
+      return;
+    if (!torrent.isInitialized()) {
+      torrent.initIfNecessary(this);
+    }
+    switch (msg.getType()) {
+      case KEEP_ALIVE:
+        // Nothing to do, we're keeping the connection open anyways.
+        break;
+      case CHOKE:
+        this.choked = true;
+        this.firePeerChoked();
+        this.cancelPendingRequests();
+        break;
+      case UNCHOKE:
+        this.choked = false;
+        logger.trace("Peer {} is now accepting requests.", this);
+        this.firePeerReady();
+        break;
+      case INTERESTED:
+        this.interested = true;
+        if (this.choking) {
+          unchoke();
+        }
+        break;
+      case NOT_INTERESTED:
+        this.interested = false;
+        if (!interesting) {
+          unbind(true);
+        }
+        break;
+      case HAVE:
+        // Record this peer has the given piece
+        PeerMessage.HaveMessage have = (PeerMessage.HaveMessage) msg;
+        Piece havePiece = this.torrent.getPiece(have.getPieceIndex());
+
+        synchronized (this.availablePiecesLock) {
+          this.availablePieces.set(havePiece.getIndex());
+          logger.trace("Peer {} now has {} [{}/{}].",
+                  new Object[]{
+                          this,
+                          havePiece,
+                          this.availablePieces.cardinality(),
+                          this.torrent.getPieceCount()
+                  });
+        }
+
+        this.firePieceAvailabity(havePiece);
+        break;
+      case BITFIELD:
+        // Augment the hasPiece bit field from this BITFIELD message
+        PeerMessage.BitfieldMessage bitfield =
+                (PeerMessage.BitfieldMessage) msg;
+
+        synchronized (this.availablePiecesLock) {
+          this.availablePieces.or(bitfield.getBitfield());
+          logger.trace("Recorded bitfield from {} with {} " +
+                          "pieces(s) [{}/{}].",
+                  new Object[]{
+                          this,
+                          bitfield.getBitfield().cardinality(),
+                          this.availablePieces.cardinality(),
+                          this.torrent.getPieceCount()
+                  });
+        }
+
+        this.fireBitfieldAvailabity();
+        break;
+      case REQUEST:
+        PeerMessage.RequestMessage request =
+                (PeerMessage.RequestMessage) msg;
+        logger.trace("Got request message for {} ({} {}@{}) from {}", new Object[]{
+                Arrays.toString(TorrentUtils.getTorrentFileNames(torrent).toArray()),
+                request.getPiece(),
+                request.getLength(),
+                request.getOffset(),
+                this
+        });
+        Piece rp = this.torrent.getPiece(request.getPiece());
+
+        // If we are choking from this peer and it still sends us
+        // requests, it is a violation of the BitTorrent protocol.
+        // Similarly, if the peer requests a piece we don't have, it
+        // is a violation of the BitTorrent protocol. In these
+        // situation, terminate the connection.
+        if (!rp.isValid()) {
+          logger.warn("Peer {} violated protocol, terminating exchange: " + this.isChoking() + " " + rp.isValid(), this);
+          this.unbind(true);
+          break;
+        }
+
+        if (request.getLength() >
+                PeerMessage.RequestMessage.MAX_REQUEST_SIZE) {
+          logger.warn("Peer {} requested a block too big, terminating exchange.", this);
+          this.unbind(true);
+          break;
+        }
+
+        // At this point we agree to send the requested piece block to
+        // the remote peer, so let's queue a message with that block
+        try {
+
+          ByteBuffer bufferForMessage = PeerMessage.PieceMessage.createBufferWithHeaderForMessage(
+                  request.getPiece(), request.getOffset(), request.getLength());
+
+          rp.read(request.getOffset(), request.getLength(), bufferForMessage);
+
+          this.send(PeerMessage.PieceMessage.craft(request.getPiece(),
+                  request.getOffset(), bufferForMessage));
+          this.upload.add(request.getLength());
+
+          if (request.getOffset() + request.getLength() == rp.size()) {
+            this.firePieceSent(rp);
+          }
+        } catch (IOException ioe) {
+          logger.debug("error", ioe);
+          this.fireIOException(new IOException(
+                  "Error while sending piece block request!", ioe));
+        }
+
+        break;
+      case PIECE:
+        // Record the incoming piece block.
+
+        // Should we keep track of the requested pieces and act when we
+        // get a piece we didn't ask for, or should we just stay
+        // greedy?
+        PeerMessage.PieceMessage piece = (PeerMessage.PieceMessage) msg;
+        Piece p = this.torrent.getPiece(piece.getPiece());
+
+        logger.trace("Got piece ({} {}@{}) from {}", new Object[]{
+                p.getIndex(),
+                p.size(),
+                piece.getOffset(),
+                this
+        });
+
+        this.download.add(piece.getBlock().capacity());
+
+        try {
+          boolean isPieceDownloaded = false;
+          synchronized (p) {
+            // Remove the corresponding request from the request queue to
+            //  make room for next block requests.
+            this.removeBlockRequest(p);
+            if (p.isValid()) {
+              this.cancelPendingRequests(p);
+              this.firePeerReady();
+              logger.trace("Discarding block for already completed " + p);
+              break;
+            }
+            //TODO add proper catch for IOException
+            p.record(piece.getBlock(), piece.getOffset());
+
+            // If the block offset equals the piece size and the block
+            // length is 0, it means the piece has been entirely
+            // downloaded. In this case, we have nothing to save, but
+            // we should validate the piece.
+            if (getRemainingRequestedPieces(p) == 0) {
+              this.firePieceCompleted(p);
+              isPieceDownloaded = true;
+            }
+          }
+          if (isPieceDownloaded) {
+            firePeerReady();
+          }
+        } catch (IOException ioe) {
+          logger.error(ioe.getMessage(), ioe);
+          this.fireIOException(new IOException(
+                  "Error while storing received piece block!", ioe));
+          break;
+        }
+        break;
+      case CANCEL:
+        // No need to support
+        break;
+    }
+  }
+
+  /**
+   * Fire the peer choked event to all registered listeners.
+   * <p/>
+   * <p>
+   * The event contains the peer that chocked.
+   * </p>
+   */
+  private void firePeerChoked() {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handlePeerChoked(this);
+    }
+  }
+
+  /**
+   * Fire the peer ready event to all registered listeners.
+   * <p/>
+   * <p>
+   * The event contains the peer that unchoked or became ready.
+   * </p>
+   */
+  private void firePeerReady() {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handlePeerReady(this);
+    }
+  }
+
+  /**
+   * Fire the piece availability event to all registered listeners.
+   * <p/>
+   * <p>
+   * The event contains the peer (this), and the piece that became available.
+   * </p>
+   */
+  private void firePieceAvailabity(Piece piece) {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handlePieceAvailability(this, piece);
+    }
+  }
+
+  /**
+   * Fire the bit field availability event to all registered listeners.
+   * <p/>
+   * The event contains the peer (this), and the bit field of available pieces
+   * from this peer.
+   */
+  private void fireBitfieldAvailabity() {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handleBitfieldAvailability(this,
+              this.getAvailablePieces());
+    }
+  }
+
+  /**
+   * Fire the piece sent event to all registered listeners.
+   * <p/>
+   * <p>
+   * The event contains the peer (this), and the piece number that was
+   * sent to the peer.
+   * </p>
+   *
+   * @param piece The completed piece.
+   */
+  private void firePieceSent(Piece piece) {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handlePieceSent(this, piece);
+    }
+  }
+
+  /**
+   * Fire the piece completion event to all registered listeners.
+   * <p/>
+   * <p>
+   * The event contains the peer (this), and the piece number that was
+   * completed.
+   * </p>
+   *
+   * @param piece The completed piece.
+   */
+  private void firePieceCompleted(Piece piece) throws IOException {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handlePieceCompleted(this, piece);
+    }
+  }
+
+  /**
+   * Fire the peer disconnected event to all registered listeners.
+   * <p/>
+   * <p>
+   * The event contains the peer that disconnected (this).
+   * </p>
+   */
+  private void firePeerDisconnected() {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handlePeerDisconnected(this);
+    }
+  }
+
+  private void afterPeerDisconnected() {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.afterPeerRemoved(this);
+    }
+  }
+
+  private void firePeerConnected() {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handleNewPeerConnected(this);
+    }
+  }
+
+  /**
+   * Fire the IOException event to all registered listeners.
+   * <p/>
+   * <p>
+   * The event contains the peer that triggered the problem, and the
+   * exception object.
+   * </p>
+   */
+  private void fireIOException(IOException ioe) {
+    for (PeerActivityListener listener : this.listeners) {
+      listener.handleIOException(this, ioe);
+    }
+  }
+
+  public SharedTorrent getTorrent() {
+    return this.torrent;
+  }
+
+  public int getDownloadingPiecesCount() {
+    synchronized (requestsLock) {
+      return myRequestedPieces.size();
+    }
+  }
+
+  /**
+   * Download rate comparator.
+   * <p/>
+   * <p>
+   * Compares sharing peers based on their current download rate.
+   * </p>
+   *
+   * @author mpetazzoni
+   * @see Rate.RateComparator
+   */
+  public static class DLRateComparator
+          implements Comparator<SharingPeer>, Serializable {
+
+    private static final long serialVersionUID = 96307229964730L;
+
+    public int compare(SharingPeer a, SharingPeer b) {
+      return Rate.RATE_COMPARATOR.compare(a.getDLRate(), b.getDLRate());
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/SharingPeerInfo.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/SharingPeerInfo.java
new file mode 100644
index 0000000..05d29d7
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/peer/SharingPeerInfo.java
@@ -0,0 +1,22 @@
+package com.turn.ttorrent.client.peer;
+
+import com.turn.ttorrent.common.TorrentHash;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author Sergey.Pak
+ * Date: 8/9/13
+ * Time: 6:40 PM
+ */
+public interface SharingPeerInfo {
+
+  String getIp();
+
+  int getPort();
+
+  TorrentHash getTorrentHash();
+
+  ByteBuffer getPeerId();
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/EmptyPieceStorageFactory.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/EmptyPieceStorageFactory.java
new file mode 100644
index 0000000..271a32c
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/EmptyPieceStorageFactory.java
@@ -0,0 +1,23 @@
+package com.turn.ttorrent.client.storage;
+
+import com.turn.ttorrent.common.TorrentMetadata;
+
+import java.util.BitSet;
+
+public class EmptyPieceStorageFactory implements PieceStorageFactory {
+
+  public static final EmptyPieceStorageFactory INSTANCE = new EmptyPieceStorageFactory();
+
+  private EmptyPieceStorageFactory() {
+  }
+
+  @Override
+  public PieceStorage createStorage(TorrentMetadata metadata, TorrentByteStorage byteStorage) {
+    return new PieceStorageImpl(
+            byteStorage,
+            new BitSet(metadata.getPiecesCount()),
+            metadata.getPiecesCount(),
+            metadata.getPieceLength()
+    );
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FairPieceStorageFactory.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FairPieceStorageFactory.java
new file mode 100644
index 0000000..3c700be
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FairPieceStorageFactory.java
@@ -0,0 +1,66 @@
+package com.turn.ttorrent.client.storage;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.common.TorrentFile;
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentUtils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.BitSet;
+
+/**
+ * This implementation will read all pieces from storage and compare hashes of pieces with really hashed
+ * from metadata
+ */
+public class FairPieceStorageFactory implements PieceStorageFactory {
+
+  public final static FairPieceStorageFactory INSTANCE = new FairPieceStorageFactory();
+
+  private FairPieceStorageFactory() {
+  }
+
+  @Override
+  public PieceStorage createStorage(TorrentMetadata metadata, TorrentByteStorage byteStorage) throws IOException {
+    long totalSize = 0;
+    for (TorrentFile file : metadata.getFiles()) {
+      totalSize += file.size;
+    }
+
+    byteStorage.open(false);
+    BitSet availablePieces = new BitSet(metadata.getPiecesCount());
+    try {
+      if (!byteStorage.isBlank()) {
+        int pieceLength = metadata.getPieceLength();
+        for (int i = 0; i < metadata.getPiecesCount(); i++) {
+          long position = (long) i * pieceLength;
+          int len;
+          if (totalSize - position > pieceLength) {
+            len = pieceLength;
+          } else {
+            len = (int) (totalSize - position);
+          }
+          if (!byteStorage.isBlank(position, len)) {
+            ByteBuffer buffer = ByteBuffer.allocate(len);
+            byteStorage.read(buffer, position);
+            byte[] expectedHash = Arrays.copyOfRange(metadata.getPiecesHashes(), i * Constants.PIECE_HASH_SIZE, (i + 1) * Constants.PIECE_HASH_SIZE);
+            byte[] actualHash = TorrentUtils.calculateSha1Hash(buffer.array());
+            if (Arrays.equals(expectedHash, actualHash)) {
+              availablePieces.set(i);
+            }
+          }
+        }
+      }
+    } finally {
+      byteStorage.close();
+    }
+
+    return new PieceStorageImpl(
+            byteStorage,
+            availablePieces,
+            metadata.getPiecesCount(),
+            metadata.getPieceLength()
+    );
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java
new file mode 100644
index 0000000..083e3e4
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileCollectionStorage.java
@@ -0,0 +1,269 @@
+/**
+ * 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.client.storage;
+
+import com.turn.ttorrent.common.TorrentFile;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.TorrentMetadata;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+
+
+/**
+ * Multi-file torrent byte storage.
+ *
+ * <p>
+ * This implementation of the torrent byte storage provides support for
+ * multi-file torrents and completely abstracts the read/write operations from
+ * the notion of different files. The byte storage is represented as one
+ * continuous byte storage, directly accessible by offset regardless of which
+ * file this offset lands.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @author dgiffin
+ */
+public class FileCollectionStorage implements TorrentByteStorage {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(FileCollectionStorage.class);
+
+  private final List<FileStorage> files;
+  private final long size;
+  private volatile boolean myIsOpen;
+
+  /**
+   * Initialize a new multi-file torrent byte storage.
+   *
+   * @param files The list of individual {@link FileStorage}
+   *              objects making up the torrent.
+   * @param size  The total size of the torrent data, in bytes.
+   */
+  public FileCollectionStorage(List<FileStorage> files,
+                               long size) {
+    this.files = files;
+    this.size = size;
+
+    logger.debug("Initialized torrent byte storage on {} file(s) " +
+            "({} total byte(s)).", files.size(), size);
+  }
+
+  public static FileCollectionStorage create(TorrentMetadata metadata, File parent) throws IOException {
+    if (!parent.isDirectory()) {
+      throw new IllegalArgumentException("Invalid parent directory!");
+    }
+    List<FileStorage> files = new LinkedList<FileStorage>();
+    long offset = 0L;
+    long totalSize = 0;
+    for (TorrentFile file : metadata.getFiles()) {
+      File actual = new File(parent, file.getRelativePathAsString());
+
+      if (!actual.getCanonicalPath().startsWith(parent.getCanonicalPath())) {
+        throw new SecurityException("Torrent file path attempted " +
+                "to break directory jail!");
+      }
+
+      if (!actual.getParentFile().exists() && !actual.getParentFile().mkdirs()) {
+        throw new IOException("Unable to create directories " + actual.getParent() + " for storing torrent file " + actual.getName());
+      }
+      files.add(new FileStorage(actual, offset, file.size));
+      offset += file.size;
+      totalSize += file.size;
+    }
+    return new FileCollectionStorage(files, totalSize);
+  }
+
+  public synchronized void open(final boolean seeder) throws IOException {
+    for (FileStorage file : files) {
+      if (!file.isOpen())
+        file.open(seeder);
+    }
+    myIsOpen = true;
+  }
+
+  @Override
+  public int read(ByteBuffer buffer, long position) throws IOException {
+    int requested = buffer.remaining();
+    int bytes = 0;
+
+    for (FileOffset fo : this.select(position, requested)) {
+      // TODO: remove cast to int when large ByteBuffer support is
+      // implemented in Java.
+      buffer.limit((int) (buffer.position() + fo.length));
+      bytes += fo.file.read(buffer, fo.offset);
+    }
+
+    if (bytes < requested) {
+      throw new IOException("Storage collection read underrun!");
+    }
+
+    return bytes;
+  }
+
+  @Override
+  public int write(ByteBuffer buffer, long position) throws IOException {
+    int requested = buffer.remaining();
+
+    int bytes = 0;
+
+    for (FileOffset fo : this.select(position, requested)) {
+      buffer.limit(bytes + (int) fo.length);
+      bytes += fo.file.write(buffer, fo.offset);
+    }
+
+    if (bytes < requested) {
+      throw new IOException("Storage collection write underrun!");
+    }
+
+    return bytes;
+  }
+
+  @Override
+  public boolean isBlank(long position, long size) {
+    for (FileOffset fo : this.select(position, size)) {
+      if (!fo.file.isBlank(fo.offset, fo.length)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public boolean isBlank() {
+    for (FileStorage file : this.files) {
+      if (!file.isBlank()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public synchronized void close() throws IOException {
+    for (FileStorage file : this.files) {
+      file.close();
+    }
+    myIsOpen = false;
+  }
+
+  @Override
+  public synchronized void finish() throws IOException {
+    for (FileStorage file : this.files) {
+      file.finish();
+    }
+  }
+
+  @Override
+  public boolean isFinished() {
+    for (FileStorage file : this.files) {
+      if (!file.isFinished()) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  @Override
+  public void delete() throws IOException {
+    for (FileStorage file : files) {
+      file.delete();
+    }
+  }
+
+  /**
+   * File operation details holder.
+   *
+   * <p>
+   * This simple inner class holds the details for a read or write operation
+   * on one of the underlying {@link FileStorage}s.
+   * </p>
+   *
+   * @author dgiffin
+   * @author mpetazzoni
+   */
+  private static class FileOffset {
+
+    public final FileStorage file;
+    public final long offset;
+    public final long length;
+
+    FileOffset(FileStorage file, long offset, long length) {
+      this.file = file;
+      this.offset = offset;
+      this.length = length;
+    }
+  }
+
+  /**
+   * Select the group of files impacted by an operation.
+   *
+   * <p>
+   * This function selects which files are impacted by a read or write
+   * operation, with their respective relative offset and chunk length.
+   * </p>
+   *
+   * @param offset The offset of the operation, in bytes, relative to the
+   *               complete byte storage.
+   * @param length The number of bytes to read or write.
+   * @return A list of {@link FileOffset} objects representing the {@link
+   * FileStorage}s impacted by the operation, bundled with their
+   * respective relative offset and number of bytes to read or write.
+   * @throws IllegalArgumentException If the offset and length go over the
+   *                                  byte storage size.
+   * @throws IllegalStateException    If the files registered with this byte
+   *                                  storage can't accommodate the request (should not happen, really).
+   */
+  private List<FileOffset> select(long offset, long length) {
+    if (offset + length > this.size) {
+      throw new IllegalArgumentException("Buffer overrun (" +
+              offset + " + " + length + " > " + this.size + ") !");
+    }
+
+    List<FileOffset> selected = new LinkedList<FileOffset>();
+    long bytes = 0;
+
+    for (FileStorage file : this.files) {
+      if (file.offset() >= offset + length) {
+        break;
+      }
+
+      if (file.offset() + file.size() < offset) {
+        continue;
+      }
+
+      long position = offset - file.offset();
+      position = position > 0 ? position : 0;
+      long size = Math.min(
+              file.size() - position,
+              length - bytes);
+      selected.add(new FileOffset(file, position, size));
+      bytes += size;
+    }
+
+    if (selected.size() == 0 || bytes < length) {
+      throw new IllegalStateException("Buffer underrun (only got " +
+              bytes + " out of " + length + " byte(s) requested)!");
+    }
+
+    return selected;
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java
new file mode 100644
index 0000000..5f0ddde
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FileStorage.java
@@ -0,0 +1,260 @@
+/**
+ * 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.client.storage;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedByInterruptException;
+import java.nio.channels.FileChannel;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+
+/**
+ * Single-file torrent byte data storage.
+ *
+ * <p>
+ * This implementation of TorrentByteStorageFile provides a torrent byte data
+ * storage relying on a single underlying file and uses a RandomAccessFile
+ * FileChannel to expose thread-safe read/write methods.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+public class FileStorage implements TorrentByteStorage {
+
+  private static final String PARTIAL_FILE_NAME_SUFFIX = ".part";
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(FileStorage.class);
+
+  private final File target;
+  private File partial;
+  private final long offset;
+  private final long size;
+
+  private RandomAccessFile raf;
+  private FileChannel channel;
+  private File current;
+  private boolean myIsOpen = false;
+  private boolean isBlank;
+
+  private final ReadWriteLock myLock = new ReentrantReadWriteLock();
+
+  public FileStorage(File file, long offset, long size) {
+    this.target = file;
+    this.offset = offset;
+    this.size = size;
+
+  }
+
+  public void open(final boolean seeder) throws IOException {
+    try {
+      myLock.writeLock().lock();
+      if (seeder) {
+        if (!target.exists()) {
+          throw new IOException("Target file " + target.getAbsolutePath() + " doesn't exist.");
+        }
+        this.current = this.target;
+        this.raf = new RandomAccessFile(this.current, "r");
+      } else {
+        this.partial = new File(this.target.getAbsolutePath() + PARTIAL_FILE_NAME_SUFFIX);
+
+        if (this.partial.exists()) {
+          logger.debug("Partial download found at {}. Continuing...",
+                  this.partial.getAbsolutePath());
+          this.current = this.partial;
+          this.isBlank = false;
+        } else if (!this.target.exists()) {
+          logger.debug("Downloading new file to {}...",
+                  this.partial.getAbsolutePath());
+          this.current = this.partial;
+          this.isBlank = true;
+        } else {
+          logger.debug("Using existing file {}.",
+                  this.target.getAbsolutePath());
+          this.current = this.target;
+          this.isBlank = false;
+        }
+        this.raf = new RandomAccessFile(this.current, "rw");
+        this.raf.setLength(this.size);
+      }
+
+      // Set the file length to the appropriate size, eventually truncating
+      // or extending the file if it already exists with a different size.
+      myIsOpen = true;
+      this.channel = raf.getChannel();
+
+      logger.debug("Opened byte storage file at {} ({}+{} byte(s)).",
+              new Object[]{
+                      this.current.getAbsolutePath(),
+                      this.offset,
+                      this.size,
+              });
+    } finally {
+      myLock.writeLock().unlock();
+    }
+  }
+
+  protected long offset() {
+    return this.offset;
+  }
+
+  public long size() {
+    return this.size;
+  }
+
+  @Override
+  public int read(ByteBuffer buffer, long position) throws IOException {
+    try {
+      myLock.readLock().lock();
+      int requested = buffer.remaining();
+
+      if (position + requested > this.size) {
+        throw new IllegalArgumentException("Invalid storage read request!");
+      }
+
+      int bytes = this.channel.read(buffer, position);
+      if (bytes < requested) {
+        throw new IOException("Storage underrun!");
+      }
+
+      return bytes;
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+  @Override
+  public int write(ByteBuffer buffer, long position) throws IOException {
+    try {
+      myLock.writeLock().lock();
+      int requested = buffer.remaining();
+      this.isBlank = false;
+
+      if (position + requested > this.size) {
+        throw new IllegalArgumentException("Invalid storage write request!");
+      }
+
+      return this.channel.write(buffer, position);
+    } finally {
+      myLock.writeLock().unlock();
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      myLock.writeLock().lock();
+      if (!myIsOpen) return;
+      logger.debug("Closing file channel to {}. Channel open: {}", current.getName(), channel.isOpen());
+      if (this.channel.isOpen()) {
+        try {
+          this.channel.force(true);
+        } catch (ClosedByInterruptException ignored) {
+        }
+      }
+      this.raf.close();
+      myIsOpen = false;
+    } finally {
+      myLock.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Move the partial file to its final location.
+   */
+  @Override
+  public void finish() throws IOException {
+    try {
+      myLock.writeLock().lock();
+      logger.debug("Closing file channel to " + this.current.getName() +
+              " (download complete).");
+      if (this.channel.isOpen()) {
+        this.channel.force(true);
+      }
+
+      // Nothing more to do if we're already on the target file.
+      if (this.isFinished()) {
+        return;
+      }
+
+      try {
+        FileUtils.deleteQuietly(this.target);
+        this.raf.close();
+        FileUtils.moveFile(this.current, this.target);
+      } catch (Exception ex) {
+        logger.error("An error occurred while moving file to its final location", ex);
+        if (this.target.exists()) {
+          throw new IOException("Was unable to delete existing file " + target.getAbsolutePath(), ex);
+        }
+        FileUtils.copyFile(this.current, this.target);
+      }
+
+      this.current = this.target;
+
+      FileUtils.deleteQuietly(this.partial);
+      myIsOpen = false;
+      logger.debug("Moved torrent data from {} to {}.",
+              this.partial.getName(),
+              this.target.getName());
+    } finally {
+      myLock.writeLock().unlock();
+    }
+  }
+
+  public boolean isOpen() {
+    try {
+      myLock.readLock().lock();
+      return myIsOpen;
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+  @Override
+  public boolean isBlank(long position, long size) {
+      return isBlank();
+  }
+
+  @Override
+  public boolean isBlank() {
+    try {
+      myLock.readLock().lock();
+      return isBlank;
+    } finally {
+      myLock.readLock().unlock();
+    }
+  }
+
+  @Override
+  public boolean isFinished() {
+    return this.current.equals(this.target);
+  }
+
+  @Override
+  public void delete() throws IOException {
+    close();
+    final File local = this.current;
+    if (local != null) local.delete();
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FullyPieceStorageFactory.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FullyPieceStorageFactory.java
new file mode 100644
index 0000000..02763fb
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/FullyPieceStorageFactory.java
@@ -0,0 +1,26 @@
+package com.turn.ttorrent.client.storage;
+
+import com.turn.ttorrent.common.TorrentMetadata;
+
+import java.util.BitSet;
+
+public class FullyPieceStorageFactory implements PieceStorageFactory {
+
+  public final static FullyPieceStorageFactory INSTANCE = new FullyPieceStorageFactory();
+
+  private FullyPieceStorageFactory() {
+  }
+
+  @Override
+  public PieceStorage createStorage(TorrentMetadata metadata, TorrentByteStorage byteStorage) {
+
+    BitSet availablePieces = new BitSet(metadata.getPiecesCount());
+    availablePieces.set(0, metadata.getPiecesCount());
+    return new PieceStorageImpl(
+            byteStorage,
+            availablePieces,
+            metadata.getPiecesCount(),
+            metadata.getPieceLength()
+    );
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorage.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorage.java
new file mode 100644
index 0000000..66318fd
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorage.java
@@ -0,0 +1,19 @@
+package com.turn.ttorrent.client.storage;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.BitSet;
+
+public interface PieceStorage extends Closeable {
+
+  void savePiece(int pieceIndex, byte[] pieceData) throws IOException;
+
+  byte[] readPiecePart(int pieceIndex, int offset, int length) throws IOException;
+
+  BitSet getAvailablePieces();
+
+  boolean isFinished();
+
+  void closeFully() throws IOException;
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorageFactory.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorageFactory.java
new file mode 100644
index 0000000..d00d44e
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorageFactory.java
@@ -0,0 +1,18 @@
+package com.turn.ttorrent.client.storage;
+
+import com.turn.ttorrent.common.TorrentMetadata;
+
+import java.io.IOException;
+
+public interface PieceStorageFactory {
+
+  /**
+   * create new {@link PieceStorage} for specified torrent with specified byte storage
+   *
+   * @param metadata    specified metadata
+   * @param byteStorage specified byte storage where will be stored pieces
+   * @return new {@link PieceStorage}
+   */
+  PieceStorage createStorage(TorrentMetadata metadata, TorrentByteStorage byteStorage) throws IOException;
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorageImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorageImpl.java
new file mode 100644
index 0000000..189d749
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/PieceStorageImpl.java
@@ -0,0 +1,173 @@
+package com.turn.ttorrent.client.storage;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.BitSet;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+public class PieceStorageImpl implements PieceStorage {
+
+  private final TorrentByteStorage fileCollectionStorage;
+  private final ReadWriteLock readWriteLock;
+
+  private final Object openStorageLock = new Object();
+
+  @Nullable
+  private volatile BitSet availablePieces;
+  private final int piecesCount;
+  private final int pieceSize;
+  private volatile boolean isOpen;
+  private volatile boolean closedFully = false;
+
+  public PieceStorageImpl(TorrentByteStorage fileCollectionStorage,
+                          BitSet availablePieces,
+                          int piecesCount,
+                          int pieceSize) {
+    this.fileCollectionStorage = fileCollectionStorage;
+    this.readWriteLock = new ReentrantReadWriteLock();
+    this.piecesCount = piecesCount;
+    this.pieceSize = pieceSize;
+    BitSet bitSet = new BitSet(piecesCount);
+    bitSet.or(availablePieces);
+    if (bitSet.cardinality() != piecesCount) {
+      this.availablePieces = bitSet;
+    }
+    isOpen = false;
+  }
+
+  private void checkPieceIndex(int pieceIndex) {
+    if (pieceIndex < 0 || pieceIndex >= piecesCount) {
+      throw new IllegalArgumentException("Incorrect piece index " + pieceIndex + ". Piece index must be positive less than" + piecesCount);
+    }
+  }
+
+  @Override
+  public void savePiece(int pieceIndex, byte[] pieceData) throws IOException {
+    checkPieceIndex(pieceIndex);
+    try {
+      readWriteLock.writeLock().lock();
+
+      if (closedFully) throw new IOException("Storage is closed");
+
+      BitSet availablePieces = this.availablePieces;
+
+      boolean isFullyDownloaded = availablePieces == null;
+
+      if (isFullyDownloaded) return;
+
+      if (availablePieces.get(pieceIndex)) return;
+
+      openStorageIsNecessary(false);
+
+      long pos = pieceIndex;
+      pos = pos * pieceSize;
+      ByteBuffer buffer = ByteBuffer.wrap(pieceData);
+      fileCollectionStorage.write(buffer, pos);
+
+      availablePieces.set(pieceIndex);
+      boolean isFullyNow = availablePieces.cardinality() == piecesCount;
+      if (isFullyNow) {
+        this.availablePieces = null;
+        fileCollectionStorage.finish();
+        fileCollectionStorage.close();
+        fileCollectionStorage.open(true);
+      }
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+  }
+
+  private void openStorageIsNecessary(boolean onlyRead) throws IOException {
+    if (!isOpen) {
+      fileCollectionStorage.open(onlyRead);
+      isOpen = true;
+    }
+  }
+
+  @Override
+  public byte[] readPiecePart(int pieceIndex, int offset, int length) throws IOException {
+    checkPieceIndex(pieceIndex);
+    try {
+      readWriteLock.readLock().lock();
+
+      if (closedFully) throw new IOException("Storage is closed");
+
+      BitSet availablePieces = this.availablePieces;
+      if (availablePieces != null && !availablePieces.get(pieceIndex)) {
+        throw new IllegalArgumentException("trying reading part of not available piece");
+      }
+
+      synchronized (openStorageLock) {
+        openStorageIsNecessary(availablePieces == null);
+      }
+
+      ByteBuffer buffer = ByteBuffer.allocate(length);
+      long pos = pieceIndex;
+      pos = pos * pieceSize + offset;
+      fileCollectionStorage.read(buffer, pos);
+      return buffer.array();
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  @Override
+  public boolean isFinished() {
+    try {
+      readWriteLock.readLock().lock();
+      return availablePieces == null;
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  @Override
+  public void closeFully() throws IOException {
+    try {
+      readWriteLock.writeLock().lock();
+      close0();
+      closedFully = true;
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+  }
+
+  @Override
+  public BitSet getAvailablePieces() {
+    try {
+      readWriteLock.readLock().lock();
+      BitSet result = new BitSet(piecesCount);
+
+      BitSet availablePieces = this.availablePieces;
+      boolean isFullyDownloaded = availablePieces == null;
+
+      if (isFullyDownloaded) {
+        result.set(0, piecesCount);
+        return result;
+      }
+      result.or(availablePieces);
+      return result;
+    } finally {
+      readWriteLock.readLock().unlock();
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      readWriteLock.writeLock().lock();
+      close0();
+    } finally {
+      readWriteLock.writeLock().unlock();
+    }
+  }
+
+  private void close0() throws IOException {
+    if (!isOpen) return;
+    fileCollectionStorage.close();
+    isOpen = false;
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/TorrentByteStorage.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/TorrentByteStorage.java
new file mode 100644
index 0000000..15ba9bc
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/storage/TorrentByteStorage.java
@@ -0,0 +1,112 @@
+/**
+ * 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.client.storage;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+
+/**
+ * Abstract torrent byte storage.
+ *
+ * <p>
+ * This interface defines the methods for accessing an abstracted torrent byte
+ * storage. A torrent, especially when it contains multiple files, needs to be
+ * seen as one single continuous stream of bytes. Torrent pieces will most
+ * likely span accross file boundaries. This abstracted byte storage aims at
+ * providing a simple interface for read/write access to the torrent data,
+ * regardless of how it is composed underneath the piece structure.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @author dgiffin
+ */
+public interface TorrentByteStorage extends Closeable {
+
+  void open(boolean seeder) throws IOException;
+
+  /**
+   * Read from the byte storage.
+   *
+   * <p>
+   * Read {@code length} bytes at position {@code position} from the underlying
+   * byte storage and return them in a {@link ByteBuffer}.
+   * </p>
+   *
+   * @param buffer   The buffer to read the bytes into. The buffer's limit will
+   *                 control how many bytes are read from the storage.
+   * @param position The position, in bytes, to read from. This must be within
+   *                 the storage boundary.
+   * @return The number of bytes read from the storage.
+   * @throws IOException If an I/O error occurs while reading from the
+   *                     byte storage.
+   */
+  int read(ByteBuffer buffer, long position) throws IOException;
+
+  /**
+   * Write bytes to the byte storage.
+   *
+   * <p>
+   * </p>
+   *
+   * @param block    A {@link ByteBuffer} containing the bytes to write to the
+   *                 storage. The buffer limit is expected to be set correctly: all bytes
+   *                 from the buffer will be used.
+   * @param position Position in the underlying byte storage to write the block
+   *                 at.
+   * @return The number of bytes written to the storage.
+   * @throws IOException If an I/O error occurs while writing to the byte
+   *                     storage.
+   */
+  int write(ByteBuffer block, long position) throws IOException;
+
+  /**
+   * Finalize the byte storage when the download is complete.
+   *
+   * <p>
+   * This gives the byte storage the opportunity to perform finalization
+   * operations when the download completes, like moving the files from a
+   * temporary location to their destination.
+   * </p>
+   *
+   * @throws IOException If the finalization failed.
+   */
+  void finish() throws IOException;
+
+  /**
+   * Tells whether this byte storage has been finalized.
+   */
+  boolean isFinished();
+
+  /**
+   * @param position Position in the underlying byte storage to write the block at.
+   * @param size Size of region to check.
+   * @return true if the region starting with positions only contains zeros
+   */
+  boolean isBlank(long position, long size);
+
+  /**
+   *
+   * @return true if the enter storage only contains zeros
+   */
+  boolean isBlank();
+
+  /**
+   * Delete byte storage information
+   */
+  void delete() throws IOException;
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/EndGameStrategy.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/EndGameStrategy.java
new file mode 100644
index 0000000..eb1c86d
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/EndGameStrategy.java
@@ -0,0 +1,28 @@
+/*
+ * 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.client.strategy;
+
+import com.turn.ttorrent.client.Piece;
+import com.turn.ttorrent.client.peer.SharingPeer;
+
+import java.util.List;
+
+public interface EndGameStrategy {
+
+  RequestsCollection collectRequests(Piece[] allPieces, List<SharingPeer> connectedPeers);
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/EndGameStrategyImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/EndGameStrategyImpl.java
new file mode 100644
index 0000000..edc0589
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/EndGameStrategyImpl.java
@@ -0,0 +1,86 @@
+/*
+ * 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.client.strategy;
+
+import com.turn.ttorrent.client.Piece;
+import com.turn.ttorrent.client.peer.SharingPeer;
+
+import java.util.*;
+
+public class EndGameStrategyImpl implements EndGameStrategy {
+
+  private static final Random RANDOM = new Random();
+
+  private final int peersPerPiece;
+
+  public EndGameStrategyImpl(int peersPerPiece) {
+    this.peersPerPiece = peersPerPiece;
+  }
+
+  @Override
+  public RequestsCollection collectRequests(Piece[] allPieces, List<SharingPeer> connectedPeers) {
+    List<SharingPeer> sorted = new ArrayList<SharingPeer>(connectedPeers);
+    Map<Piece, List<SharingPeer>> selectedPieces = new HashMap<Piece, List<SharingPeer>>();
+    Collections.sort(sorted, new Comparator<SharingPeer>() {
+      @Override
+      public int compare(SharingPeer o1, SharingPeer o2) {
+        return Integer.valueOf(o1.getDownloadedPiecesCount()).compareTo(o2.getDownloadedPiecesCount());
+      }
+    });
+    for (Piece piece : allPieces) {
+      if (piece.isValid()) continue;
+
+      //if we don't have piece, then request this piece from two random peers
+      //(peers are selected by peer rank, peer with better rank will be selected more often then peer with bad rank
+      List<SharingPeer> selectedPeers = selectGoodPeers(piece, peersPerPiece, sorted);
+      selectedPieces.put(piece, selectedPeers);
+    }
+    return new RequestsCollectionImpl(selectedPieces);
+  }
+
+  private List<SharingPeer> selectGoodPeers(Piece piece, int count, List<SharingPeer> sortedPeers) {
+    List<SharingPeer> notSelected = new ArrayList<SharingPeer>(sortedPeers);
+    Iterator<SharingPeer> iterator = notSelected.iterator();
+    while (iterator.hasNext()) {
+      SharingPeer peer = iterator.next();
+      boolean peerHasCurrentPiece = peer.getAvailablePieces().get(piece.getIndex());
+      boolean alreadyRequested = peer.getRequestedPieces().contains(piece);
+
+      if (!peerHasCurrentPiece || alreadyRequested) iterator.remove();
+    }
+    if (notSelected.size() <= count) return notSelected;
+
+    List<SharingPeer> selected = new ArrayList<SharingPeer>();
+    for (int i = 0; i < count; i++) {
+      SharingPeer sharingPeer = selectPeer(notSelected);
+      if (sharingPeer == null) continue;
+      notSelected.remove(sharingPeer);
+      selected.add(sharingPeer);
+    }
+
+    return selected;
+  }
+
+  private SharingPeer selectPeer(List<SharingPeer> notSelected) {
+    for (SharingPeer sharingPeer : notSelected) {
+      if (RANDOM.nextDouble() < 0.8) {
+        return sharingPeer;
+      }
+    }
+    return notSelected.get(RANDOM.nextInt(notSelected.size()));
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategy.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategy.java
new file mode 100644
index 0000000..5313b86
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategy.java
@@ -0,0 +1,22 @@
+package com.turn.ttorrent.client.strategy;
+
+import com.turn.ttorrent.client.Piece;
+
+import java.util.BitSet;
+
+/**
+ * Interface for a piece request strategy provider.
+ *
+ * @author cjmalloy
+ */
+public interface RequestStrategy {
+
+  /**
+   * Choose a piece from the remaining pieces.
+   *
+   * @param interesting A set of the index of all interesting pieces
+   * @param pieces      The complete array of pieces
+   * @return The chosen piece, or <code>null</code> if no piece is interesting
+   */
+  Piece choosePiece(BitSet interesting, Piece[] pieces);
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategyImplAnyInteresting.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategyImplAnyInteresting.java
new file mode 100644
index 0000000..5ba8ae4
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategyImplAnyInteresting.java
@@ -0,0 +1,23 @@
+package com.turn.ttorrent.client.strategy;
+
+import com.turn.ttorrent.client.Piece;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Random;
+
+public class RequestStrategyImplAnyInteresting implements RequestStrategy {
+
+  private final Random myRandom = new Random();
+
+  @Override
+  public Piece choosePiece(BitSet interesting, Piece[] pieces) {
+    List<Piece> onlyInterestingPieces = new ArrayList<Piece>();
+    for (Piece p : pieces) {
+      if (interesting.get(p.getIndex())) onlyInterestingPieces.add(p);
+    }
+    if (onlyInterestingPieces.isEmpty()) return null;
+    return onlyInterestingPieces.get(myRandom.nextInt(onlyInterestingPieces.size()));
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategyImplSequential.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategyImplSequential.java
new file mode 100644
index 0000000..eabc24d
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestStrategyImplSequential.java
@@ -0,0 +1,22 @@
+package com.turn.ttorrent.client.strategy;
+
+import com.turn.ttorrent.client.Piece;
+
+import java.util.BitSet;
+
+/**
+ * A sequential request strategy implementation.
+ *
+ * @author cjmalloy
+ */
+public class RequestStrategyImplSequential implements RequestStrategy {
+
+  @Override
+  public Piece choosePiece(BitSet interesting, Piece[] pieces) {
+
+    for (Piece p : pieces) {
+      if (interesting.get(p.getIndex())) return p;
+    }
+    return null;
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestsCollection.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestsCollection.java
new file mode 100644
index 0000000..9b41849
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestsCollection.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.client.strategy;
+
+public interface RequestsCollection {
+
+  void sendAllRequests();
+
+  final class Empty implements RequestsCollection {
+
+    public final static Empty INSTANCE = new Empty();
+
+    private Empty() {
+    }
+
+    @Override
+    public void sendAllRequests() {
+      //do nothing
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestsCollectionImpl.java b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestsCollectionImpl.java
new file mode 100644
index 0000000..6d18e3a
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/main/java/com/turn/ttorrent/client/strategy/RequestsCollectionImpl.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.client.strategy;
+
+import com.turn.ttorrent.client.Piece;
+import com.turn.ttorrent.client.peer.SharingPeer;
+
+import java.util.List;
+import java.util.Map;
+
+public class RequestsCollectionImpl implements RequestsCollection {
+
+  private final Map<Piece, List<SharingPeer>> selectedPieces;
+
+  public RequestsCollectionImpl(Map<Piece, List<SharingPeer>> selectedPieces) {
+    this.selectedPieces = selectedPieces;
+  }
+
+  @Override
+  public void sendAllRequests() {
+    for (Map.Entry<Piece, List<SharingPeer>> entry : selectedPieces.entrySet()) {
+      Piece piece = entry.getKey();
+      for (SharingPeer sharingPeer : entry.getValue()) {
+        sharingPeer.downloadPiece(piece);
+      }
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/ByteArrayStorage.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/ByteArrayStorage.java
new file mode 100644
index 0000000..17831f9
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/ByteArrayStorage.java
@@ -0,0 +1,77 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.client.storage.TorrentByteStorage;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class ByteArrayStorage implements TorrentByteStorage {
+
+  private final byte[] array;
+  private boolean finished = false;
+  private boolean isBlank;
+
+  public ByteArrayStorage(int maxSize) {
+    array = new byte[maxSize];
+    isBlank = true;
+  }
+
+  @Override
+  public void open(boolean seeder) {
+  }
+
+  private int intPosition(long position) {
+    if (position > Integer.MAX_VALUE || position < 0) {
+      throw new IllegalArgumentException("Position is too large");
+    }
+    return (int) position;
+  }
+
+  @Override
+  public int read(ByteBuffer buffer, long position) {
+
+    int pos = intPosition(position);
+    int bytesCount = buffer.remaining();
+    buffer.put(Arrays.copyOfRange(array, pos, pos + bytesCount));
+    return bytesCount;
+  }
+
+  @Override
+  public int write(ByteBuffer block, long position) {
+    int pos = intPosition(position);
+    int bytesCount = block.remaining();
+    byte[] toWrite = new byte[bytesCount];
+    block.get(toWrite);
+    System.arraycopy(toWrite, 0, array, pos, toWrite.length);
+    isBlank = false;
+    return bytesCount;
+  }
+
+  @Override
+  public void finish() {
+    finished = true;
+  }
+
+  @Override
+  public boolean isFinished() {
+    return finished;
+  }
+
+  @Override
+  public boolean isBlank(long position, long size) {
+    return isBlank;
+  }
+
+  @Override
+  public boolean isBlank() {
+    return isBlank;
+  }
+
+  @Override
+  public void delete() {
+  }
+
+  @Override
+  public void close() {
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/EventDispatcherTest.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/EventDispatcherTest.java
new file mode 100644
index 0000000..01de8eb
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/EventDispatcherTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.client;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertEquals;
+
+@Test
+public class EventDispatcherTest {
+
+  private EventDispatcher eventDispatcher;
+  private PeerInformation peerInfo;
+  private PieceInformation pieceInfo;
+
+  @BeforeMethod
+  public void setUp() {
+    eventDispatcher = new EventDispatcher();
+
+    peerInfo = mock(PeerInformation.class);
+    pieceInfo = mock(PieceInformation.class);
+  }
+
+  public void testWithoutListeners() {
+
+    eventDispatcher.multicaster().downloadFailed(new RuntimeException());
+    eventDispatcher.multicaster().peerConnected(peerInfo);
+    eventDispatcher.multicaster().validationComplete(1, 4);
+    eventDispatcher.multicaster().pieceDownloaded(pieceInfo, peerInfo);
+    eventDispatcher.multicaster().downloadComplete();
+    eventDispatcher.multicaster().pieceReceived(pieceInfo, peerInfo);
+    eventDispatcher.multicaster().peerDisconnected(peerInfo);
+  }
+
+  public void testInvocation() {
+
+    final AtomicInteger invocationCount = new AtomicInteger();
+    int count = 5;
+    for (int i = 0; i < count; i++) {
+      eventDispatcher.addListener(new TorrentListenerWrapper() {
+        @Override
+        public void downloadComplete() {
+          invocationCount.incrementAndGet();
+        }
+      });
+    }
+
+    eventDispatcher.multicaster().peerConnected(peerInfo);
+
+    assertEquals(invocationCount.get(), 0);
+
+    eventDispatcher.multicaster().downloadComplete();
+
+    assertEquals(invocationCount.get(), count);
+
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/PeersStorageTest.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/PeersStorageTest.java
new file mode 100644
index 0000000..af55e8f
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/PeersStorageTest.java
@@ -0,0 +1,71 @@
+package com.turn.ttorrent.client;
+
+import com.turn.ttorrent.client.peer.PeerActivityListener;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.PeerUID;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.net.InetSocketAddress;
+import java.nio.channels.ByteChannel;
+import java.util.Collection;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+@Test
+public class PeersStorageTest {
+
+  private PeersStorage myPeersStorage;
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myPeersStorage = new PeersStorage();
+  }
+
+  public void getSetSelfTest() {
+
+    assertNull(myPeersStorage.getSelf());
+    Peer self = new Peer("", 1);
+    myPeersStorage.setSelf(self);
+    assertEquals(myPeersStorage.getSelf(), self);
+  }
+
+  public void testThatPeersStorageReturnNewCollection() {
+    SharingPeer sharingPeer = getMockSharingPeer();
+    myPeersStorage.putIfAbsent(new PeerUID(new InetSocketAddress("127.0.0.1", 6881), ""), sharingPeer);
+    Collection<SharingPeer> sharingPeers = myPeersStorage.getSharingPeers();
+
+    assertEquals(1, myPeersStorage.getSharingPeers().size());
+    assertEquals(1, sharingPeers.size());
+
+    sharingPeers.add(sharingPeer);
+
+    assertEquals(1, myPeersStorage.getSharingPeers().size());
+    assertEquals(2, sharingPeers.size());
+  }
+
+  private SharingPeer getMockSharingPeer() {
+    return new SharingPeer("1",
+            1,
+            null,
+            mock(SharedTorrent.class),
+            null,
+            mock(PeerActivityListener.class),
+            mock(ByteChannel.class), "TO", 1234);
+  }
+
+  public void getAndRemoveSharingPeersTest() {
+    SharingPeer sharingPeer = getMockSharingPeer();
+    PeerUID peerUid = new PeerUID(new InetSocketAddress("127.0.0.1", 6881), "");
+    SharingPeer oldPeer = myPeersStorage.putIfAbsent(peerUid, sharingPeer);
+
+    assertNull(oldPeer);
+    assertEquals(myPeersStorage.getSharingPeer(peerUid), sharingPeer);
+
+    assertEquals(myPeersStorage.removeSharingPeer(peerUid), sharingPeer);
+    assertNull(myPeersStorage.removeSharingPeer(peerUid));
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/network/HandshakeReceiverTest.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/network/HandshakeReceiverTest.java
new file mode 100644
index 0000000..2b3849a
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/network/HandshakeReceiverTest.java
@@ -0,0 +1,187 @@
+package com.turn.ttorrent.client.network;
+
+import com.turn.ttorrent.TempFiles;
+import com.turn.ttorrent.Utils;
+import com.turn.ttorrent.client.*;
+import com.turn.ttorrent.client.peer.PeerActivityListener;
+import com.turn.ttorrent.client.peer.SharingPeer;
+import com.turn.ttorrent.client.storage.FairPieceStorageFactory;
+import com.turn.ttorrent.client.storage.FileCollectionStorage;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentCreator;
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentSerializer;
+import com.turn.ttorrent.network.ConnectionManager;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
+import java.nio.channels.Pipe;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.*;
+
+@Test
+public class HandshakeReceiverTest {
+
+  private HandshakeReceiver myHandshakeReceiver;
+  private byte[] mySelfId;
+  private Context myContext;
+  private TempFiles myTempFiles;
+
+  public HandshakeReceiverTest() {
+    if (Logger.getRootLogger().getAllAppenders().hasMoreElements())
+      return;
+    BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("[%d{MMdd HH:mm:ss,SSS} %t] %6p - %20.20c - %m %n")));
+  }
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myTempFiles = new TempFiles();
+    Logger.getRootLogger().setLevel(Utils.getLogLevel());
+    mySelfId = "selfId1selfId2selfId".getBytes();
+    ByteBuffer selfId = ByteBuffer.wrap(mySelfId);
+    myContext = mock(Context.class);
+    PeersStorage peersStorage = new PeersStorage();
+    TorrentsStorage torrentsStorage = new TorrentsStorage();
+    when(myContext.getPeersStorage()).thenReturn(peersStorage);
+    when(myContext.getTorrentsStorage()).thenReturn(torrentsStorage);
+    peersStorage.setSelf(new Peer("127.0.0.1", 54645, selfId));
+    CommunicationManager communicationManager = mock(CommunicationManager.class);
+    when(communicationManager.getConnectionManager()).thenReturn(mock(ConnectionManager.class));
+    myHandshakeReceiver = new HandshakeReceiver(
+            myContext,
+            "127.0.0.1",
+            45664,
+            false);
+  }
+
+  @AfterMethod
+  public void tearDown() throws Exception {
+    myTempFiles.cleanup();
+  }
+
+  public void testReceiveHandshake() throws Exception {
+    Pipe p1 = Pipe.open();
+    Pipe p2 = Pipe.open();
+    ByteChannel client = new ByteSourceChannel(p1.source(), p2.sink());
+    ByteChannel server = new ByteSourceChannel(p2.source(), p1.sink());
+    String peerIdStr = "peerIdpeerIdpeerId22";
+    String torrentHashStr = "torrenttorrenttorren";
+    byte[] peerId = peerIdStr.getBytes();
+    byte[] torrentHash = torrentHashStr.getBytes();
+    Handshake hs = Handshake.craft(torrentHash, peerId);
+
+    if (hs == null) {
+      fail("Handshake instance is null");
+    }
+
+    ByteBuffer byteBuffer = hs.getData();
+    client.write(byteBuffer);
+
+    final File tempFile = myTempFiles.createTempFile(1024 * 1024);
+    TorrentMetadata torrent = TorrentCreator.create(tempFile, URI.create(""), "test");
+    File torrentFile = myTempFiles.createTempFile();
+    FileOutputStream fos = new FileOutputStream(torrentFile);
+    fos.write(new TorrentSerializer().serialize(torrent));
+    fos.close();
+
+    final LoadedTorrent loadedTorrent = mock(LoadedTorrent.class);
+    final SharedTorrent sharedTorrent =
+            SharedTorrent.fromFile(torrentFile,
+                    FairPieceStorageFactory.INSTANCE.createStorage(torrent, FileCollectionStorage.create(torrent, tempFile.getParentFile())),
+                    loadedTorrent.getTorrentStatistic());
+
+    TorrentLoader torrentsLoader = mock(TorrentLoader.class);
+    when(torrentsLoader.loadTorrent(loadedTorrent)).thenReturn(sharedTorrent);
+    when(myContext.getTorrentLoader()).thenReturn(torrentsLoader);
+    final ExecutorService executorService = Executors.newFixedThreadPool(1);
+    when(myContext.getExecutor()).thenReturn(executorService);
+    myContext.getTorrentsStorage().addTorrent(hs.getHexInfoHash(), loadedTorrent);
+
+    final AtomicBoolean onConnectionEstablishedInvoker = new AtomicBoolean(false);
+
+    final Semaphore semaphore = new Semaphore(0);
+    when(myContext.createSharingPeer(any(String.class),
+            anyInt(),
+            any(ByteBuffer.class),
+            any(SharedTorrent.class),
+            any(ByteChannel.class),
+            any(String.class),
+            anyInt()))
+            .thenReturn(new SharingPeer("127.0.0.1", 6881, ByteBuffer.wrap(peerId), sharedTorrent, null,
+                    mock(PeerActivityListener.class), server, "TO", 1234) {
+              @Override
+              public void onConnectionEstablished() {
+                onConnectionEstablishedInvoker.set(true);
+                semaphore.release();
+              }
+            });
+
+    PeersStorage peersStorage = myContext.getPeersStorage();
+    assertEquals(0, myContext.getTorrentsStorage().activeTorrents().size());
+    assertEquals(peersStorage.getSharingPeers().size(), 0);
+    myHandshakeReceiver.processAndGetNext(server);
+    assertEquals(peersStorage.getSharingPeers().size(), 1);
+    ByteBuffer answer = ByteBuffer.allocate(byteBuffer.capacity());
+    client.read(answer);
+    answer.rewind();
+    Handshake answerHs = Handshake.parse(answer);
+    assertEquals(answerHs.getPeerId(), mySelfId);
+    semaphore.tryAcquire(1, TimeUnit.SECONDS);
+    assertTrue(onConnectionEstablishedInvoker.get());
+    executorService.shutdown();
+  }
+
+  // TODO: 11/15/17 bad tests (e.g. incorrect torrentID, incorrect handshake, etc
+
+  private static class ByteSourceChannel implements ByteChannel {
+
+    private final Pipe.SourceChannel readChannel;
+    private final Pipe.SinkChannel writeChannel;
+
+    public ByteSourceChannel(Pipe.SourceChannel readChannel, Pipe.SinkChannel writeChannel) {
+      this.readChannel = readChannel;
+      this.writeChannel = writeChannel;
+    }
+
+    @Override
+    public int read(ByteBuffer dst) throws IOException {
+      return readChannel.read(dst);
+    }
+
+    @Override
+    public int write(ByteBuffer src) throws IOException {
+      return this.writeChannel.write(src);
+    }
+
+    @Override
+    public boolean isOpen() {
+      throw new RuntimeException("not implemented");
+    }
+
+    @Override
+    public void close() throws IOException {
+      readChannel.close();
+      writeChannel.close();
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FairPieceStorageFactoryTest.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FairPieceStorageFactoryTest.java
new file mode 100644
index 0000000..6e35507
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FairPieceStorageFactoryTest.java
@@ -0,0 +1,101 @@
+package com.turn.ttorrent.client.storage;
+
+import com.turn.ttorrent.common.TorrentFile;
+import com.turn.ttorrent.common.TorrentMetadata;
+import org.jetbrains.annotations.NotNull;
+import org.testng.annotations.Test;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+@Test
+public class FairPieceStorageFactoryTest {
+
+  public void testCreatingStorageForLargeFile() throws Exception {
+
+
+    int pieceLength = 1024 * 1024;
+    int pieceCount = 3000;
+
+    //last piece can have not fully size
+    final int lastPieceLength = pieceLength / 2;
+    long totalSize = (long) (pieceCount - 1) * pieceLength + lastPieceLength;
+
+    TorrentMetadata metadata = mock(TorrentMetadata.class);
+    when(metadata.getPieceLength()).thenReturn(pieceLength);
+    when(metadata.getPiecesCount()).thenReturn(pieceCount);
+    when(metadata.getFiles()).thenReturn(Collections.singletonList(new TorrentFile(Collections.singletonList("test.avi"), totalSize, "")));
+    when(metadata.getPiecesHashes()).thenReturn(new byte[pieceCount * 20]);
+
+    final AtomicBoolean isReadInvokedForLastPiece = new AtomicBoolean(false);
+    TorrentByteStorage storage = new TorrentByteStorage() {
+      @Override
+      public void open(boolean seeder) {
+
+      }
+
+      @Override
+      public int read(ByteBuffer buffer, long position) {
+        if (buffer.capacity() == lastPieceLength) {
+          isReadInvokedForLastPiece.set(true);
+        }
+        buffer.putInt(1);
+        return 1;
+      }
+
+      @Override
+      public int write(ByteBuffer block, long position) {
+        throw notImplemented();
+      }
+
+      @NotNull
+      private RuntimeException notImplemented() {
+        return new RuntimeException("notImplemented");
+      }
+
+      @Override
+      public void finish() {
+        throw notImplemented();
+      }
+
+      @Override
+      public boolean isFinished() {
+        throw notImplemented();
+      }
+
+      @Override
+      public boolean isBlank(long position, long size) {
+        return false;
+      }
+
+      @Override
+      public boolean isBlank() {
+        return false;
+      }
+
+      @Override
+      public void delete() {
+        throw notImplemented();
+      }
+
+      @Override
+      public void close() {
+
+      }
+    };
+
+    PieceStorage pieceStorage = FairPieceStorageFactory.INSTANCE.createStorage(metadata, storage);
+
+    assertTrue(isReadInvokedForLastPiece.get());
+    assertEquals(0, pieceStorage.getAvailablePieces().cardinality());
+    assertFalse(pieceStorage.isFinished());
+  }
+
+}
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileCollectionStorageTest.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileCollectionStorageTest.java
new file mode 100644
index 0000000..60db718
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/FileCollectionStorageTest.java
@@ -0,0 +1,89 @@
+package com.turn.ttorrent.client.storage;
+
+import com.turn.ttorrent.TempFiles;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * User: loyd
+ * Date: 11/24/13
+ */
+public class FileCollectionStorageTest {
+
+  private TempFiles tempFiles;
+
+  @BeforeMethod
+  public void setUp() {
+    tempFiles = new TempFiles();
+  }
+
+  @AfterMethod
+  public void tearDown() {
+    tempFiles.cleanup();
+  }
+
+  @Test
+  public void testSelect() throws Exception {
+    final File file1 = tempFiles.createTempFile();
+    final File file2 = tempFiles.createTempFile();
+
+    final List<FileStorage> files = new ArrayList<FileStorage>();
+    files.add(new FileStorage(file1, 0, 2));
+    files.add(new FileStorage(file2, 2, 2));
+    final FileCollectionStorage storage = new FileCollectionStorage(files, 4);
+
+    storage.open(false);
+    try {
+      // since all of these files already exist, we are considered finished
+      assertTrue(storage.isFinished());
+
+      // write to first file works
+      write(new byte[]{1, 2}, 0, storage);
+      check(new byte[]{1, 2}, file1);
+
+      // write to second file works
+      write(new byte[]{5, 6}, 2, storage);
+      check(new byte[]{5, 6}, file2);
+
+      // write to two files works
+      write(new byte[]{8, 9, 10, 11}, 0, storage);
+      check(new byte[]{8, 9}, file1);
+      check(new byte[]{10, 11}, file2);
+
+      // make sure partial write into next file works
+      write(new byte[]{100, 101, 102}, 0, storage);
+      check(new byte[]{102, 11}, file2);
+    } finally {
+      storage.close();
+    }
+  }
+
+  private void write(byte[] bytes, int offset, FileCollectionStorage storage) throws IOException {
+    storage.write(ByteBuffer.wrap(bytes), offset);
+    storage.finish();
+  }
+
+  private void check(byte[] bytes, File f) throws IOException {
+    final byte[] temp = new byte[bytes.length];
+    FileInputStream fileInputStream = new FileInputStream(f);
+    final int totalRead;
+    try {
+      totalRead = fileInputStream.read(temp);
+    } finally {
+      fileInputStream.close();
+    }
+    assertEquals(totalRead, temp.length);
+    assertEquals(temp, bytes);
+  }
+}
\ No newline at end of file
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/PieceStorageImplTest.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/PieceStorageImplTest.java
new file mode 100644
index 0000000..9cac770
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/storage/PieceStorageImplTest.java
@@ -0,0 +1,84 @@
+package com.turn.ttorrent.client.storage;
+
+import com.turn.ttorrent.client.ByteArrayStorage;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.BitSet;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class PieceStorageImplTest {
+
+  private PieceStorage pieceStorage;
+  private int pieceSize;
+  private int pieceCount;
+  private byte[] allPieces;
+
+  @BeforeMethod
+  public void setUp() throws IOException {
+
+    pieceSize = 12;
+    pieceCount = 8;
+    ByteArrayStorage storage = new ByteArrayStorage(pieceSize * pieceCount);
+    pieceStorage = new PieceStorageImpl(storage, new BitSet(), pieceCount, pieceSize);
+    allPieces = new byte[pieceCount * pieceSize];
+    for (byte i = 0; i < allPieces.length; i++) {
+      allPieces[i] = i;
+    }
+  }
+
+  @Test
+  public void testStorage() throws IOException {
+
+    assertEquals(pieceStorage.getAvailablePieces().cardinality(), 0);
+    byte[] firstPieceData = Arrays.copyOfRange(allPieces, pieceSize, 2 * pieceSize);
+    pieceStorage.savePiece(1, firstPieceData);
+    byte[] thirdPieceData = Arrays.copyOfRange(allPieces, 3 * pieceSize, 4 * pieceSize);
+    pieceStorage.savePiece(3, thirdPieceData);
+
+    BitSet availablePieces = pieceStorage.getAvailablePieces();
+    assertEquals(availablePieces.cardinality(), 2);
+    assertTrue(availablePieces.get(1));
+    assertTrue(availablePieces.get(3));
+
+    byte[] actualFirstPieceData = pieceStorage.readPiecePart(1, 0, pieceSize);
+    byte[] actualThirdPieceData = pieceStorage.readPiecePart(3, 0, pieceSize);
+    assertEquals(actualFirstPieceData, firstPieceData);
+    assertEquals(actualThirdPieceData, thirdPieceData);
+
+    //check that reading by parts works correctly
+    byte[] firstPiecePart = pieceStorage.readPiecePart(1, 0, pieceSize / 2);
+    byte[] secondPiecePart = pieceStorage.readPiecePart(1, pieceSize / 2, pieceSize / 2);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    outputStream.write(firstPiecePart);
+    outputStream.write(secondPiecePart);
+    assertEquals(firstPieceData, outputStream.toByteArray());
+
+  }
+
+  @Test
+  public void testFullStorage() throws IOException {
+    assertEquals(pieceStorage.getAvailablePieces().cardinality(), 0);
+    for (int i = 0; i < pieceCount; i++) {
+      assertEquals(pieceStorage.getAvailablePieces().cardinality(), i);
+      pieceStorage.savePiece(i, Arrays.copyOfRange(allPieces, i * pieceSize, (i + 1) * pieceSize));
+    }
+    assertEquals(pieceStorage.getAvailablePieces().cardinality(), pieceCount);
+  }
+
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void testReadUnavailablePiece() throws IOException {
+    pieceStorage.readPiecePart(45, 0, pieceSize);
+  }
+
+  @AfterMethod
+  public void tearDown() throws IOException {
+    pieceStorage.close();
+  }
+}
diff --git a/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/strategy/RequestStrategyImplAnyInterestingTest.java b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/strategy/RequestStrategyImplAnyInterestingTest.java
new file mode 100644
index 0000000..56457b7
--- /dev/null
+++ b/ttorrent-master/ttorrent-client/src/test/java/com/turn/ttorrent/client/strategy/RequestStrategyImplAnyInterestingTest.java
@@ -0,0 +1,53 @@
+package com.turn.ttorrent.client.strategy;
+
+import com.turn.ttorrent.client.Piece;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.BitSet;
+import java.util.SortedSet;
+
+public class RequestStrategyImplAnyInterestingTest {
+
+  private final SortedSet<Piece> myRarest = null;//myRarest don't need for it strategy
+  private final int myPiecesTotal = 10;
+  private final Piece[] myPieces = new Piece[myPiecesTotal];
+  private final RequestStrategy myRequestStrategy = new RequestStrategyImplAnyInteresting();
+
+  @BeforeClass
+  public void init() {
+    for (int i = 0; i < myPieces.length; i++) {
+      myPieces[i] = new Piece(null, i, 0, new byte[0]);
+    }
+  }
+
+  @Test
+  public void choosePieceNoInterestingTest() {
+    Piece actual = myRequestStrategy.choosePiece(new BitSet(), myPieces);
+    Assert.assertNull(actual);
+  }
+
+  @Test
+  public void choosePieceOneInterestingTest() {
+    BitSet interesting = new BitSet();
+    for (int i = 0; i < myPieces.length; i++) {
+      interesting.clear();
+      interesting.set(i);
+      Piece expected = myPieces[i];
+      Piece actual = myRequestStrategy.choosePiece(interesting, myPieces);
+      Assert.assertEquals(expected, actual);
+    }
+  }
+
+  @Test
+  public void choosePieceTest() {
+    BitSet interesting = new BitSet();
+    int interestingFrom = 1;
+    int interestingTo = 5;
+    interesting.set(interestingFrom, interestingTo);
+    Piece actual = myRequestStrategy.choosePiece(interesting, myPieces);
+    Assert.assertTrue(actual.getIndex() >= interestingFrom && actual.getIndex() <= interestingTo);
+  }
+
+}
diff --git a/ttorrent-master/ttorrent-tracker/pom.xml b/ttorrent-master/ttorrent-tracker/pom.xml
new file mode 100644
index 0000000..0eb1beb
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/pom.xml
@@ -0,0 +1,40 @@
+<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/tracker</name>
+    <url>http://turn.github.com/ttorrent/</url>
+    <artifactId>ttorrent-tracker</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-common</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-bencoding</artifactId>
+            <version>1.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.turn</groupId>
+            <artifactId>ttorrent-test-api</artifactId>
+            <version>1.0</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/AddressChecker.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/AddressChecker.java
new file mode 100644
index 0000000..98241ea
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/AddressChecker.java
@@ -0,0 +1,14 @@
+package com.turn.ttorrent.tracker;
+
+public interface AddressChecker {
+
+  /**
+   * this method must return true if is incorrect ip and other peers can not connect to this peer. If this method return true
+   * tracker doesn't register the peer for current torrent
+   *
+   * 检验传入的peer ip是否是正确的
+   * @param ip specified address
+   */
+  boolean isBadAddress(String ip);
+
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/MultiAnnounceRequestProcessor.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/MultiAnnounceRequestProcessor.java
new file mode 100644
index 0000000..5bcbd4e
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/MultiAnnounceRequestProcessor.java
@@ -0,0 +1,59 @@
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPTrackerErrorMessage;
+import org.simpleframework.http.Status;
+import org.slf4j.Logger;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class MultiAnnounceRequestProcessor {
+
+  private final TrackerRequestProcessor myTrackerRequestProcessor;
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(MultiAnnounceRequestProcessor.class);
+
+  public MultiAnnounceRequestProcessor(TrackerRequestProcessor trackerRequestProcessor) {
+    myTrackerRequestProcessor = trackerRequestProcessor;
+  }
+
+  public void process(final String body, final String url, final String hostAddress, final TrackerRequestProcessor.RequestHandler requestHandler) throws IOException {
+
+    final List<BEValue> responseMessages = new ArrayList<BEValue>();
+    final AtomicBoolean isAnySuccess = new AtomicBoolean(false);
+    for (String s : body.split("\n")) {
+      myTrackerRequestProcessor.process(s, hostAddress, new TrackerRequestProcessor.RequestHandler() {
+        @Override
+        public void serveResponse(int code, String description, ByteBuffer responseData) {
+          isAnySuccess.set(isAnySuccess.get() || (code == Status.OK.getCode()));
+          try {
+            responseMessages.add(BDecoder.bdecode(responseData));
+          } catch (IOException e) {
+            logger.warn("cannot decode message from byte buffer");
+          }
+        }
+      });
+    }
+    if (responseMessages.isEmpty()) {
+      ByteBuffer res;
+      Status status;
+      res = HTTPTrackerErrorMessage.craft("").getData();
+      status = Status.BAD_REQUEST;
+      requestHandler.serveResponse(status.getCode(), "", res);
+      return;
+    }
+    final ByteArrayOutputStream out = new ByteArrayOutputStream();
+    BEncoder.bencode(responseMessages, out);
+    requestHandler.serveResponse(isAnySuccess.get() ? Status.OK.getCode() : Status.BAD_REQUEST.getCode(), "", ByteBuffer.wrap(out.toByteArray()));
+  }
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/PeerCollectorThread.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/PeerCollectorThread.java
new file mode 100644
index 0000000..bdbd0e2
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/PeerCollectorThread.java
@@ -0,0 +1,38 @@
+package com.turn.ttorrent.tracker;
+
+/**
+ * The unfresh peer collector thread.
+ * <p>
+ * <p>
+ * Every PEER_COLLECTION_FREQUENCY_SECONDS, this thread will collect
+ * unfresh peers from all announced torrents.
+ * </p>
+ */
+
+// 周期性地清理不再活跃的 peers
+public class PeerCollectorThread extends Thread {
+
+  public static final int COLLECTION_FREQUENCY = 10;
+  private final TorrentsRepository myTorrentsRepository;
+  private volatile int myTorrentExpireTimeoutSec = 20 * 60;
+
+  public PeerCollectorThread(TorrentsRepository torrentsRepository) {
+    myTorrentsRepository = torrentsRepository;
+  }
+
+  public void setTorrentExpireTimeoutSec(int torrentExpireTimeoutSec) {
+    myTorrentExpireTimeoutSec = torrentExpireTimeoutSec;
+  }
+
+  @Override
+  public void run() {
+    while (!isInterrupted()) {
+      myTorrentsRepository.cleanup(myTorrentExpireTimeoutSec);
+      try {
+        Thread.sleep(COLLECTION_FREQUENCY * 1000);
+      } catch (InterruptedException ie) {
+        break;
+      }
+    }
+  }
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TorrentsRepository.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TorrentsRepository.java
new file mode 100644
index 0000000..65ae892
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TorrentsRepository.java
@@ -0,0 +1,99 @@
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+// 管理和存储 torrents(种子文件)信息的仓库,它实现了对 torrents 的增、查、更新、清理等操作。
+// 这个类的实现主要用于一个 Tracker 服务器,负责管理 torrent 信息并对参与下载和上传的 peer 进行跟踪。
+
+//torrent存储仓库,能根据infohash的值,获取唯一的torrent,能实现上传
+// torrent 相关信息:整个 BT 种子文件实际上就是一个 bencoding 编码的 Dictionary, 它有两个 Key, 分别是 announce 和 info
+// announce是btTracker的url
+// info是字典,对应资源相关信息,name,piece length(分片长度)
+//      piece(处理将数据分片)
+//      length / files(这两个键不能同时存在, 有且仅有其中一个,只是一个文件是length,文件夹是files)
+
+public class TorrentsRepository {
+
+    private final ReentrantLock[] myLocks;
+    //  ConcurrentMap一种线程安全的map映射,键值对应,torrent 的哈希值--TrackedTorrent
+    private final ConcurrentMap<String, TrackedTorrent> myTorrents;
+
+    // 构造函数
+    public TorrentsRepository(int locksCount) {
+
+        if (locksCount <= 0) {
+            throw new IllegalArgumentException("Lock count must be positive");
+        }
+
+        myLocks = new ReentrantLock[locksCount];
+        for (int i = 0; i < myLocks.length; i++) {
+            myLocks[i] = new ReentrantLock();
+        }
+        myTorrents = new ConcurrentHashMap<String, TrackedTorrent>();
+    }
+
+    //  根据传入的种子的哈希值hexInfoHash,获取TrackedTorrent对象
+    public TrackedTorrent getTorrent(String hexInfoHash) {
+        return myTorrents.get(hexInfoHash);
+    }
+
+    // torrent不存在,进行添加操作
+    public void putIfAbsent(String hexInfoHash, TrackedTorrent torrent) {
+        myTorrents.putIfAbsent(hexInfoHash, torrent);
+        //调用myTorrents类型ConcurrentMap下的添加函数
+    }
+    // 更新torrent数据
+    public TrackedTorrent putIfAbsentAndUpdate(String hexInfoHash, TrackedTorrent torrent,
+                                               AnnounceRequestMessage.RequestEvent event, ByteBuffer peerId,
+                                               String hexPeerId, String ip, int port, long uploaded, long downloaded,
+                                               long left) throws UnsupportedEncodingException {
+        TrackedTorrent actualTorrent;
+        try {
+            lockFor(hexInfoHash).lock();
+            TrackedTorrent oldTorrent = myTorrents.putIfAbsent(hexInfoHash, torrent);
+            actualTorrent = oldTorrent == null ? torrent : oldTorrent;
+            actualTorrent.update(event, peerId, hexPeerId, ip, port, uploaded, downloaded, left);
+        } finally {
+            lockFor(hexInfoHash).unlock();
+        }
+        return actualTorrent;
+    }
+
+    // 锁,避免阻塞其他torrent操作
+    private ReentrantLock lockFor(String torrentHash) {
+        return myLocks[Math.abs(torrentHash.hashCode()) % myLocks.length];
+    }
+
+    @SuppressWarnings("unused")
+    public void clear() {
+        myTorrents.clear();
+    }
+
+    public void cleanup(int torrentExpireTimeoutSec) {
+        for (TrackedTorrent trackedTorrent : myTorrents.values()) {
+            try {
+                lockFor(trackedTorrent.getHexInfoHash()).lock();
+                trackedTorrent.collectUnfreshPeers(torrentExpireTimeoutSec);
+                if (trackedTorrent.getPeers().size() == 0) {
+                    myTorrents.remove(trackedTorrent.getHexInfoHash());
+                }
+            } finally {
+                lockFor(trackedTorrent.getHexInfoHash()).unlock();
+            }
+        }
+    }
+
+
+    public Map<String, TrackedTorrent> getTorrents() {
+        return new HashMap<String, TrackedTorrent>(myTorrents);
+    }
+
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java
new file mode 100644
index 0000000..d8057c5
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedPeer.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright (C) 2011-2012 Turn, Inc.
+ * <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.tracker;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.common.*;
+import org.slf4j.Logger;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * A BitTorrent tracker peer.
+ * <p>
+ * <p>
+ * Represents a peer exchanging on a given torrent. In this implementation,
+ * we don't really care about the status of the peers and how much they
+ * have downloaded / exchanged because we are not a torrent exchange and
+ * don't need to keep track of what peers are doing while they're
+ * downloading. We only care about when they start, and when they are done.
+ * </p>
+ * <p>
+ * <p>
+ * We also never expire peers automatically. Unless peers send a STOPPED
+ * announce request, they remain as long as the torrent object they are a
+ * part of.
+ * </p>
+ */
+
+// 跟踪peer(对等结点,相当于使用该tracker的用户)
+/* 管理一个TrackedTorrent种子的相关的用户对象,能帮助在下载时候找到对象。
+* 新下载用户完成下载后被标记为完成,可以添加
+* */
+// 管理用户登录时间
+public class TrackedPeer extends Peer {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(TrackedPeer.class);
+
+  private final TimeService myTimeService;
+  private long uploaded;
+  private long downloaded;
+  private long left;
+  private TrackedTorrent torrent;//为什么包含这个实例??
+
+  /**
+   * Represents the state of a peer exchanging on this torrent.
+   * 展示用户和种子间的状态关系
+   * <p>
+   * <p>
+   * Peers can be in the STARTED state, meaning they have announced
+   * themselves to us and are eventually exchanging data with other peers.
+   * Note that a peer starting with a completed file will also be in the
+   * started state and will never notify as being in the completed state.
+   * This information can be inferred from the fact that the peer reports 0
+   * bytes left to download.
+   * </p>
+   * <p>
+   * <p>
+   * Peers enter the COMPLETED state when they announce they have entirely
+   * downloaded the file. As stated above, we may also elect them for this
+   * state if they report 0 bytes left to download.
+   * </p>
+   * <p>
+   * <p>
+   * Peers enter the STOPPED state very briefly before being removed. We
+   * still pass them to the STOPPED state in case someone else kept a
+   * reference on them.
+   * </p>
+   */
+  public enum PeerState {
+    UNKNOWN,
+    STARTED,
+    COMPLETED,
+    STOPPED
+  }
+
+  private PeerState state;
+  private long lastAnnounce; //登录时间记录
+
+  /**
+   * Instantiate a new tracked peer for the given torrent.
+   * 为给定的 torrent 实例化一个新的跟踪对等点。
+   *
+   * @param torrent The torrent this peer exchanges on.
+   * @param ip      The peer's IP address.
+   * @param port    The peer's port.
+   * @param peerId  The byte-encoded peer ID.
+   */
+  public TrackedPeer(TrackedTorrent torrent, String ip, int port, ByteBuffer peerId) {
+    this(torrent, ip, port, peerId, new SystemTimeService());
+  }
+
+  TrackedPeer(TrackedTorrent torrent, String ip, int port,
+              ByteBuffer peerId, TimeService timeService) {
+    super(ip, port, peerId);
+    myTimeService = timeService;
+    this.torrent = torrent;
+
+    // Instantiated peers start in the UNKNOWN state.
+    this.state = PeerState.UNKNOWN;
+    this.lastAnnounce = myTimeService.now();
+
+    this.uploaded = 0;
+    this.downloaded = 0;
+    this.left = 0;
+  }
+
+  /**
+   * Update this peer's state and information.
+   * <p>
+   * <p>
+   * <b>Note:</b> if the peer reports 0 bytes left to download, its state will
+   * be automatically be set to COMPLETED.
+   * </p>
+   *
+   * @param state      The peer's state.
+   * @param uploaded   Uploaded byte count, as reported by the peer.
+   * @param downloaded Downloaded byte count, as reported by the peer.
+   * @param left       Left-to-download byte count, as reported by the peer.
+   */
+  public void update(PeerState state, long uploaded, long downloaded,
+                     long left) {
+
+    // 自动检测下载完成
+    if (PeerState.STARTED.equals(state) && left == 0) {
+      state = PeerState.COMPLETED;
+    }
+
+    if (!state.equals(this.state)) {
+      logger.trace("Peer {} {} download of {}.",
+              new Object[]{
+                      this,
+                      state.name().toLowerCase(),
+                      this.torrent,
+              });
+    }
+
+    this.state = state;
+    this.lastAnnounce = myTimeService.now();
+    this.uploaded = uploaded;
+    this.downloaded = downloaded;
+    this.left = left;
+  }
+
+  /**
+   * Tells whether this peer has completed its download and can thus be
+   * considered a seeder.
+   * 告知这个peer是否完成上传,能否作为一个seeder种子持有者
+   */
+  public boolean isCompleted() {
+    return PeerState.COMPLETED.equals(this.state);
+  }
+
+  /**
+   * Returns how many bytes the peer reported it has uploaded so far.
+   */
+  public long getUploaded() {
+    return this.uploaded;
+  }
+
+  /**
+   * Returns how many bytes the peer reported it has downloaded so far.
+   */
+  public long getDownloaded() {
+    return this.downloaded;
+  }
+
+  /**
+   * Returns how many bytes the peer reported it needs to retrieve before
+   * its download is complete.
+   * 目前还剩余多少内容没有下载
+   */
+  public long getLeft() {
+    return this.left;
+  }
+
+  /**
+   * Tells whether this peer has checked in with the tracker recently.
+   * <p>
+   * <p>
+   * Non-fresh peers are automatically terminated and collected by the
+   * Tracker.
+   * 用户是否活跃
+   * </p>
+   */
+  public boolean isFresh(int expireTimeoutSec) {
+    return this.lastAnnounce + expireTimeoutSec * 1000 > myTimeService.now();
+  }
+
+  /**
+   * Returns a BEValue representing this peer for inclusion in an
+   * announce reply from the tracker.
+   * <p>
+   * The returned BEValue is a dictionary containing the peer ID (in its
+   * original byte-encoded form), the peer's IP and the peer's port.
+   */
+  public BEValue toBEValue() throws UnsupportedEncodingException {
+    Map<String, BEValue> peer = new HashMap<String, BEValue>();
+    if (this.hasPeerId()) {
+      peer.put("peer id", new BEValue(this.getPeerIdArray()));
+    }
+    peer.put("ip", new BEValue(this.getIp(), Constants.BYTE_ENCODING));
+    peer.put("port", new BEValue(this.getPort()));
+    return new BEValue(peer);
+  }
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java
new file mode 100644
index 0000000..26d8972
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackedTorrent.java
@@ -0,0 +1,303 @@
+/**
+ * Copyright (C) 2011-2012 Turn, Inc.
+ * <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.tracker;
+
+import com.turn.ttorrent.common.*;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage.RequestEvent;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Tracked torrents are torrent for which we don't expect to have data files
+ * for.
+ * <p>
+ * <p>
+ * {@link TrackedTorrent} objects are used by the BitTorrent tracker to
+ * represent a torrent that is announced by the tracker. As such, it is not
+ * expected to point to any valid local data like. It also contains some
+ * additional information used by the tracker to keep track of which peers
+ * exchange on it, etc.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+
+//表示一个被 BitTorrent 跟踪器(tracker)跟踪的种子(torrent)。
+// 这个类并不涉及实际的数据文件,而是用于管理与该种子相关的连接(peers)以及跟踪种子状态的信息。
+// 实现用户获取一个种子的peer值,方便进行分片下载
+
+public class TrackedTorrent implements TorrentHash {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(TrackedTorrent.class);
+
+  /**
+   * Minimum announce interval requested from peers, in seconds.
+   */
+  public static final int MIN_ANNOUNCE_INTERVAL_SECONDS = 5;
+
+  /**
+   * Default number of peers included in a tracker response.
+   */
+  private static final int DEFAULT_ANSWER_NUM_PEERS = 30;
+
+  /**
+   * Default announce interval requested from peers, in seconds.
+   */
+  private static final int DEFAULT_ANNOUNCE_INTERVAL_SECONDS = 10;
+
+  private int answerPeers;
+  private int announceInterval;
+
+  private final byte[] info_hash;
+  //表示种子的哈希值(通过种子文件的元数据得到)。
+  // info 键对应的值生成的 SHA1 哈希, 该哈希值可作为所要请求的资源的标识符
+
+  /**
+   * Peers currently exchanging on this torrent.
+   * 目前服务器中存在的用户
+   */
+  private ConcurrentMap<PeerUID, TrackedPeer> peers;
+
+  /**
+   * Create a new tracked torrent from meta-info binary data.
+   * 创建空TrackedTorrent
+   *
+   * @param info_hash The meta-info byte data.
+   *                  encoded and hashed back to create the torrent's SHA-1 hash.
+   *                  available.
+   */
+  public TrackedTorrent(byte[] info_hash) {
+    this.info_hash = info_hash;
+
+    this.peers = new ConcurrentHashMap<PeerUID, TrackedPeer>();
+    this.answerPeers = TrackedTorrent.DEFAULT_ANSWER_NUM_PEERS;
+    this.announceInterval = TrackedTorrent.DEFAULT_ANNOUNCE_INTERVAL_SECONDS;
+  }
+
+  /**
+   * Returns the map of all peers currently exchanging on this torrent.
+   */
+  public Map<PeerUID, TrackedPeer> getPeers() {
+    return this.peers;
+  }
+
+  /**
+   * Add a peer exchanging on this torrent.
+   *
+   * @param peer The new Peer involved with this torrent.
+   */
+  public void addPeer(TrackedPeer peer) {
+    this.peers.put(new PeerUID(peer.getAddress(), this.getHexInfoHash()), peer);
+  }
+
+  public TrackedPeer getPeer(PeerUID peerUID) {
+    return this.peers.get(peerUID);
+  }
+
+  public TrackedPeer removePeer(PeerUID peerUID) {
+    return this.peers.remove(peerUID);
+  }
+
+  /**
+   * Count the number of seeders (peers in the COMPLETED state) on this
+   * torrent.
+   */
+  public int seeders() {
+    int count = 0;
+    for (TrackedPeer peer : this.peers.values()) {
+      if (peer.isCompleted()) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  /**
+   * Count the number of leechers (non-COMPLETED peers) on this torrent.
+   */
+  public int leechers() {
+    int count = 0;
+    for (TrackedPeer peer : this.peers.values()) {
+      if (!peer.isCompleted()) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  /**
+   * Remove unfresh peers from this torrent.
+   * <p>
+   * <p>
+   * Collect and remove all non-fresh peers from this torrent. This is
+   * usually called by the periodic peer collector of the BitTorrent tracker.
+   * </p>
+   */
+  public void collectUnfreshPeers(int expireTimeoutSec) {
+    for (TrackedPeer peer : this.peers.values()) {
+      if (!peer.isFresh(expireTimeoutSec)) {
+        this.peers.remove(new PeerUID(peer.getAddress(), this.getHexInfoHash()));
+      }
+    }
+  }
+
+  /**
+   * Get the announce interval for this torrent.
+   */
+  public int getAnnounceInterval() {
+    return this.announceInterval;
+  }
+
+  /**
+   * Set the announce interval for this torrent.
+   *
+   * @param interval New announce interval, in seconds.
+   */
+  public void setAnnounceInterval(int interval) {
+    if (interval <= 0) {
+      throw new IllegalArgumentException("Invalid announce interval");
+    }
+
+    this.announceInterval = interval;
+  }
+
+  /**
+   * Update this torrent's swarm from an announce event.
+   * <p>
+   * <p>
+   * This will automatically create a new peer on a 'started' announce event,
+   * and remove the peer on a 'stopped' announce event.
+   * </p>
+   *
+   * @param event      The reported event. If <em>null</em>, means a regular
+   *                   interval announce event, as defined in the BitTorrent specification.
+   * @param peerId     The byte-encoded peer ID.
+   * @param hexPeerId  The hexadecimal representation of the peer's ID.
+   * @param ip         The peer's IP address.
+   * @param port       The peer's inbound port.
+   * @param uploaded   The peer's reported uploaded byte count.
+   * @param downloaded The peer's reported downloaded byte count.
+   * @param left       The peer's reported left to download byte count.
+   * @return The peer that sent us the announce request.
+   */
+  public TrackedPeer update(RequestEvent event, ByteBuffer peerId,
+                            String hexPeerId, String ip, int port, long uploaded, long downloaded,
+                            long left) throws UnsupportedEncodingException {
+    logger.trace("event {}, Peer: {}:{}", new Object[]{event.getEventName(), ip, port});
+    TrackedPeer peer = null;
+    TrackedPeer.PeerState state = TrackedPeer.PeerState.UNKNOWN;
+
+    PeerUID peerUID = new PeerUID(new InetSocketAddress(ip, port), getHexInfoHash());
+    if (RequestEvent.STARTED.equals(event)) {
+      state = TrackedPeer.PeerState.STARTED;
+    } else if (RequestEvent.STOPPED.equals(event)) {
+      peer = this.removePeer(peerUID);
+      state = TrackedPeer.PeerState.STOPPED;
+    } else if (RequestEvent.COMPLETED.equals(event)) {
+      peer = this.getPeer(peerUID);
+      state = TrackedPeer.PeerState.COMPLETED;
+    } else if (RequestEvent.NONE.equals(event)) {
+      peer = this.getPeer(peerUID);
+      state = TrackedPeer.PeerState.STARTED;
+    } else {
+      throw new IllegalArgumentException("Unexpected announce event type!");
+    }
+
+    if (peer == null) {
+      peer = new TrackedPeer(this, ip, port, peerId);
+      this.addPeer(peer);
+    }
+    peer.update(state, uploaded, downloaded, left);
+    return peer;
+  }
+
+  /**
+   * Get a list of peers we can return in an announce response for this
+   * torrent.
+   *
+   * @param peer The peer making the request, so we can exclude it from the
+   *             list of returned peers.
+   * @return A list of peers we can include in an announce response.
+   */
+  public List<Peer> getSomePeers(Peer peer) {
+    List<Peer> peers = new LinkedList<Peer>();
+
+    // Extract answerPeers random peers
+    List<TrackedPeer> candidates = new LinkedList<TrackedPeer>(this.peers.values());
+    Collections.shuffle(candidates);
+
+    int count = 0;
+    for (TrackedPeer candidate : candidates) {
+      // Don't include the requesting peer in the answer.
+      if (peer != null && peer.looksLike(candidate)) {
+        continue;
+      }
+
+      // Only serve at most ANSWER_NUM_PEERS peers
+      if (count++ > this.answerPeers) {
+        break;
+      }
+
+      peers.add(candidate);
+    }
+
+    return peers;
+  }
+
+  /**
+   * Load a tracked torrent from the given torrent file.
+   *
+   * @param torrent The abstract {@link File} object representing the
+   *                <tt>.torrent</tt> file to load.
+   * @throws IOException              When the torrent file cannot be read.
+   */
+  public static TrackedTorrent load(File torrent) throws IOException {
+
+    TorrentMetadata torrentMetadata = new TorrentParser().parseFromFile(torrent);
+    return new TrackedTorrent(torrentMetadata.getInfoHash());
+  }
+
+
+  // 返回种子哈希值
+  @Override
+  public byte[] getInfoHash() {
+    return this.info_hash;
+  }
+
+  @Override
+  public String getHexInfoHash() {
+    return TorrentUtils.byteArrayToHexString(this.info_hash);
+  }
+
+  @Override
+  public String toString() {
+    return "TrackedTorrent{" +
+            "info_hash=" + getHexInfoHash() +
+            '}';
+  }
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/Tracker.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/Tracker.java
new file mode 100644
index 0000000..fc178f9
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/Tracker.java
@@ -0,0 +1,289 @@
+/**
+ * Copyright (C) 2011-2012 Turn, Inc.
+ * <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.
+ */
+
+// tracker客户端
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.simpleframework.http.core.ContainerServer;
+import org.simpleframework.transport.connect.Connection;
+import org.simpleframework.transport.connect.SocketConnection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * BitTorrent tracker.
+ * <p>
+ * <p>
+ * The tracker usually listens on port 6969 (the standard BitTorrent tracker
+ * port). Torrents must be registered directly to this tracker with the
+ * {@link #announce(TrackedTorrent torrent)}</code> method.
+ * </p>
+ *
+ * @author mpetazzoni
+ */
+
+// 翻译:tracker 监听6969端口,种子被TrackedTorrent类表示
+public class Tracker {
+
+  private static final Logger logger = TorrentLoggerFactory.getLogger(Tracker.class);// 日志记录
+
+  /**
+   * Request path handled by the tracker announce request handler.
+   * 由跟踪器公告请求处理程序处理的请求路径  保存路径端口ip
+   */
+  public static final String ANNOUNCE_URL = "/announce";
+
+  /**
+   * Default tracker listening port (BitTorrent's default is 6969).
+   */
+  public static final int DEFAULT_TRACKER_PORT = 6969;
+
+  /**
+   * Default server name and version announced by the tracker.
+   */
+  public static final String DEFAULT_VERSION_STRING = "BitTorrent Tracker (ttorrent)";
+
+  private Connection connection;
+
+  /**
+   * The in-memory repository of torrents tracked.
+   */
+  // 种子仓库管理
+  private final TorrentsRepository myTorrentsRepository;
+
+  private PeerCollectorThread myPeerCollectorThread;
+  private boolean stop;
+  private String myAnnounceUrl;
+  private final int myPort;
+  private SocketAddress myBoundAddress = null;
+
+  private final TrackerServiceContainer myTrackerServiceContainer;
+
+  /**
+   * Create a new BitTorrent tracker listening at the given address.
+   *
+   * @throws IOException Throws an <em>IOException</em> if the tracker
+   *                     cannot be initialized.
+   */
+  //新建tracker的声明 port是运行端口
+  public Tracker(int port) throws IOException {
+    this(port,
+            getDefaultAnnounceUrl(new InetSocketAddress(InetAddress.getLocalHost(), port)).toString()
+    );
+    //调用下一个声明 很多数据是自动生成的,创建tracker的时候只要声明默认端口
+  }
+
+  public Tracker(int port, String announceURL) throws IOException {
+    myPort = port;
+    myAnnounceUrl = announceURL;
+    myTorrentsRepository = new TorrentsRepository(10);
+    final TrackerRequestProcessor requestProcessor = new TrackerRequestProcessor(myTorrentsRepository);
+    myTrackerServiceContainer = new TrackerServiceContainer(requestProcessor,
+            new MultiAnnounceRequestProcessor(requestProcessor));
+    myPeerCollectorThread = new PeerCollectorThread(myTorrentsRepository);
+  }
+
+  public Tracker(int port, String announceURL, TrackerRequestProcessor requestProcessor, TorrentsRepository torrentsRepository) throws IOException {
+    myPort = port;
+    myAnnounceUrl = announceURL;
+    myTorrentsRepository = torrentsRepository;
+    myTrackerServiceContainer = new TrackerServiceContainer(requestProcessor, new MultiAnnounceRequestProcessor(requestProcessor));
+    myPeerCollectorThread = new PeerCollectorThread(myTorrentsRepository);
+  }
+
+  /**
+   * Returns the full announce URL served by this tracker.
+   * <p>
+   * <p>
+   * This has the form http://host:port/announce.
+   * </p>
+   */
+  private static URL getDefaultAnnounceUrl(InetSocketAddress address) {
+    try {
+      return new URL("http",
+              address.getAddress().getCanonicalHostName(),
+              address.getPort(),
+              ANNOUNCE_URL);
+    } catch (MalformedURLException mue) {
+      logger.error("Could not build tracker URL: {}!", mue, mue);
+    }
+
+    return null;
+  }
+
+  public String getAnnounceUrl() {
+    return myAnnounceUrl;
+  }
+
+  public URI getAnnounceURI() {
+    try {
+      URL announceURL = new URL(getAnnounceUrl());
+      if (announceURL != null) {
+        return announceURL.toURI();
+      }
+    } catch (URISyntaxException e) {
+      logger.error("Cannot convert announce URL to URI", e);
+    } catch (MalformedURLException e) {
+      logger.error("Cannot create URL from announceURL", e);
+    }
+    return null;
+  }
+
+  /**
+   * Start the tracker thread.
+   */
+  public void start(final boolean startPeerCleaningThread) throws IOException {
+    logger.info("Starting BitTorrent tracker on {}...",
+            getAnnounceUrl());
+    connection = new SocketConnection(new ContainerServer(myTrackerServiceContainer));
+
+    List<SocketAddress> tries = new ArrayList<SocketAddress>() {{
+      try {
+        add(new InetSocketAddress(InetAddress.getByAddress(new byte[4]), myPort));
+      } catch (Exception ex) {
+      }
+      try {
+        add(new InetSocketAddress(InetAddress.getLocalHost(), myPort));
+      } catch (Exception ex) {
+      }
+      try {
+        add(new InetSocketAddress(InetAddress.getByName(new URL(getAnnounceUrl()).getHost()), myPort));
+      } catch (Exception ex) {
+      }
+    }};
+
+    boolean started = false;
+    for (SocketAddress address : tries) {
+      try {
+        if ((myBoundAddress = connection.connect(address)) != null) {
+          logger.info("Started torrent tracker on {}", address);
+          started = true;
+          break;
+        }
+      } catch (IOException ioe) {
+        logger.info("Can't start the tracker using address{} : ", address.toString(), ioe.getMessage());
+      }
+    }
+    if (!started) {
+      logger.error("Cannot start tracker on port {}. Stopping now...", myPort);
+      stop();
+      return;
+    }
+    if (startPeerCleaningThread) {
+      if (myPeerCollectorThread == null || !myPeerCollectorThread.isAlive() || myPeerCollectorThread.getState() != Thread.State.NEW) {
+        myPeerCollectorThread = new PeerCollectorThread(myTorrentsRepository);
+      }
+
+      myPeerCollectorThread.setName("peer-peerCollectorThread:" + myPort);
+      myPeerCollectorThread.start();
+    }
+  }
+
+  /**
+   * Stop the tracker.
+   * <p>
+   * <p>
+   * This effectively closes the listening HTTP connection to terminate
+   * the service, and interrupts the peer myPeerCollectorThread thread as well.
+   * </p>
+   */
+  public void stop() {
+    this.stop = true;
+
+    try {
+      this.connection.close();
+      logger.info("BitTorrent tracker closed.");
+    } catch (IOException ioe) {
+      logger.error("Could not stop the tracker: {}!", ioe.getMessage());
+    }
+
+    if (myPeerCollectorThread != null && myPeerCollectorThread.isAlive()) {
+      myPeerCollectorThread.interrupt();
+      try {
+        myPeerCollectorThread.join();
+      } catch (InterruptedException e) {
+        //
+      }
+      logger.info("Peer collection terminated.");
+    }
+  }
+
+  /**
+   * Announce a new torrent on this tracker.
+   * <p>
+   * <p>
+   * The fact that torrents must be announced here first makes this tracker a
+   * closed BitTorrent tracker: it will only accept clients for torrents it
+   * knows about, and this list of torrents is managed by the program
+   * instrumenting this Tracker class.
+   * </p>
+   *
+   * @param torrent The Torrent object to start tracking.
+   * @return The torrent object for this torrent on this tracker. This may be
+   * different from the supplied Torrent object if the tracker already
+   * contained a torrent with the same hash.
+   */
+
+  //synchronized 确保多线程访问共享资源不会出错
+  public synchronized TrackedTorrent announce(TrackedTorrent torrent) {
+    TrackedTorrent existing = myTorrentsRepository.getTorrent(torrent.getHexInfoHash());
+
+    if (existing != null) {
+      logger.warn("Tracker already announced torrent with hash {}.", existing.getHexInfoHash());
+      return existing;
+    }
+
+    myTorrentsRepository.putIfAbsent(torrent.getHexInfoHash(), torrent);
+    logger.info("Registered new torrent with hash {}.", torrent.getHexInfoHash());
+    return torrent;
+  }
+
+  /**
+   * Set to true to allow this tracker to track external torrents (i.e. those that were not explicitly announced here).
+   *
+   * @param acceptForeignTorrents true to accept foreign torrents (false otherwise)
+   */
+  public void setAcceptForeignTorrents(boolean acceptForeignTorrents) {
+    myTrackerServiceContainer.setAcceptForeignTorrents(acceptForeignTorrents);
+  }
+
+  /**
+   * @return all tracked torrents.
+   */
+  public Collection<TrackedTorrent> getTrackedTorrents() {
+    return Collections.unmodifiableCollection(myTorrentsRepository.getTorrents().values());
+  }
+
+  public TrackedTorrent getTrackedTorrent(String hash) {
+    return myTorrentsRepository.getTorrent(hash);
+  }
+
+  public void setAnnounceInterval(int announceInterval) {
+    myTrackerServiceContainer.setAnnounceInterval(announceInterval);
+  }
+
+  public void setPeerCollectorExpireTimeout(int expireTimeout) {
+    myPeerCollectorThread.setTorrentExpireTimeoutSec(expireTimeout);
+  }
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerRequestProcessor.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerRequestProcessor.java
new file mode 100644
index 0000000..afb51fb
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerRequestProcessor.java
@@ -0,0 +1,330 @@
+/**
+ * Copyright (C) 2011-2012 Turn, Inc.
+ * <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.tracker;
+
+import com.turn.ttorrent.Constants;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.Peer;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage.ErrorMessage;
+import com.turn.ttorrent.common.protocol.TrackerMessage.MessageValidationException;
+import com.turn.ttorrent.common.protocol.http.HTTPAnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPAnnounceResponseMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPTrackerErrorMessage;
+import org.simpleframework.http.Status;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Tracker service to serve the tracker's announce requests.
+ * announce 客户端和tracker服务器进行通信
+ * <p>
+ * <p>
+ * It only serves announce requests on /announce, and only serves torrents the
+ * {@link Tracker} it serves knows about.
+ * </p>
+ * <p>
+ * <p>
+ * The list of torrents {@see #requestHandler.getTorrentsMap()} is a map of torrent hashes to their
+ * corresponding Torrent objects, and is maintained by the {@link Tracker} this
+ * service is part of. The TrackerRequestProcessor only has a reference to this map, and
+ * does not modify it.
+ * </p>
+ *
+ * @author mpetazzoni
+ * @see <a href="http://wiki.theory.org/BitTorrentSpecification">BitTorrent protocol specification</a>
+ */
+public class TrackerRequestProcessor {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(TrackerRequestProcessor.class);
+
+  /**
+   * The list of announce request URL fields that need to be interpreted as
+   * numeric and thus converted as such in the request message parsing.
+   */
+  private static final String[] NUMERIC_REQUEST_FIELDS =
+          new String[]{
+                  "port", "uploaded", "downloaded", "left",
+                  "compact", "no_peer_id", "numwant"
+          };
+  private static final int SEEDER_ANNOUNCE_INTERVAL = 150;
+  //  seeder通知间隔
+
+  private boolean myAcceptForeignTorrents = true; //default to true
+  private int myAnnounceInterval = 60; //default value
+  private final AddressChecker myAddressChecker; //ip地址检查器
+  private final TorrentsRepository myTorrentsRepository;
+
+
+  /**
+   * Create a new TrackerRequestProcessor serving the given torrents.
+   * 是一个对种子仓库进行管理的类
+   */
+  public TrackerRequestProcessor(TorrentsRepository torrentsRepository) {
+    this(torrentsRepository, new AddressChecker() {
+      @Override
+      public boolean isBadAddress(String ip) {
+        return false;
+      }
+    });
+  }
+
+  public TrackerRequestProcessor(TorrentsRepository torrentsRepository, AddressChecker addressChecker) {
+    myTorrentsRepository = torrentsRepository;
+    myAddressChecker = addressChecker;
+  }
+
+  /**
+   * Process the announce request.
+   * 处理请求
+   * <p>
+   * <p>
+   * This method attemps to read and parse the incoming announce request into
+   * an announce request message, then creates the appropriate announce
+   * response message and sends it back to the client.
+   * </p>
+   */
+  //传入数据uri=request.getAddress().toString(), 是发出请求的客户端地址包含顺便传入的参数,第二个参数是主机地址
+  //该函数的目的是处理种子文件的请求(例如,用户上传或下载种子文件时发送的公告请求)
+  public void process(final String uri, final String hostAddress, RequestHandler requestHandler)
+          throws IOException {
+    // Prepare the response headers.
+
+    /**
+     * Parse the query parameters into an announce request message.
+     *
+     * We need to rely on our own query parsing function because
+     * SimpleHTTP's Query map will contain UTF-8 decoded parameters, which
+     * doesn't work well for the byte-encoded strings we expect.
+     */
+    HTTPAnnounceRequestMessage announceRequest; //包含各种工具操作
+    try {
+      announceRequest = this.parseQuery(uri, hostAddress);
+    } catch (MessageValidationException mve) {
+      LoggerUtils.warnAndDebugDetails(logger, "Unable to parse request message. Request url is {}", uri, mve);
+      serveError(Status.BAD_REQUEST, mve.getMessage(), requestHandler);
+      return;
+    }
+
+    AnnounceRequestMessage.RequestEvent event = announceRequest.getEvent();//获取当前状态,比如完成或者开始,停止
+
+    if (event == null) {
+      event = AnnounceRequestMessage.RequestEvent.NONE;
+    }
+    TrackedTorrent torrent = myTorrentsRepository.getTorrent(announceRequest.getHexInfoHash());
+
+    // The requested torrent must be announced by the tracker if and only if myAcceptForeignTorrents is false
+    if (!myAcceptForeignTorrents && torrent == null) {
+      logger.warn("Requested torrent hash was: {}", announceRequest.getHexInfoHash());
+      serveError(Status.BAD_REQUEST, ErrorMessage.FailureReason.UNKNOWN_TORRENT, requestHandler);
+      return;
+    }
+
+    final boolean isSeeder = (event == AnnounceRequestMessage.RequestEvent.COMPLETED)
+            || (announceRequest.getLeft() == 0);//判断是做中还是下载
+
+    if (myAddressChecker.isBadAddress(announceRequest.getIp())) {//黑名单用户
+      if (torrent == null) {
+        writeEmptyResponse(announceRequest, requestHandler);
+      } else {
+        writeAnnounceResponse(torrent, null, isSeeder, requestHandler);
+      }
+      return;
+    }
+
+    final Peer peer = new Peer(announceRequest.getIp(), announceRequest.getPort());
+
+    try {
+      torrent = myTorrentsRepository.putIfAbsentAndUpdate(announceRequest.getHexInfoHash(),
+              new TrackedTorrent(announceRequest.getInfoHash()),
+              event,
+              ByteBuffer.wrap(announceRequest.getPeerId()),
+              announceRequest.getHexPeerId(),
+              announceRequest.getIp(),
+              announceRequest.getPort(),
+              announceRequest.getUploaded(),
+              announceRequest.getDownloaded(),
+              announceRequest.getLeft());
+    } catch (IllegalArgumentException iae) {
+      LoggerUtils.warnAndDebugDetails(logger, "Unable to update peer torrent. Request url is {}", uri, iae);
+      serveError(Status.BAD_REQUEST, ErrorMessage.FailureReason.INVALID_EVENT, requestHandler);
+      return;
+    }
+
+    // Craft and output the answer
+    writeAnnounceResponse(torrent, peer, isSeeder, requestHandler);
+  }
+
+  private void writeEmptyResponse(HTTPAnnounceRequestMessage announceRequest, RequestHandler requestHandler) throws IOException {
+    HTTPAnnounceResponseMessage announceResponse;
+    try {
+      announceResponse = HTTPAnnounceResponseMessage.craft(
+              myAnnounceInterval,
+              0,
+              0,
+              Collections.<Peer>emptyList(),
+              announceRequest.getHexInfoHash());
+      requestHandler.serveResponse(Status.OK.getCode(), Status.OK.getDescription(), announceResponse.getData());
+    } catch (Exception e) {
+      serveError(Status.INTERNAL_SERVER_ERROR, e.getMessage(), requestHandler);
+    }
+  }
+
+  public void setAnnounceInterval(int announceInterval) {
+    myAnnounceInterval = announceInterval;
+  }
+
+  public int getAnnounceInterval() {
+    return myAnnounceInterval;
+  }
+
+  private void writeAnnounceResponse(TrackedTorrent torrent, Peer peer, boolean isSeeder, RequestHandler requestHandler) throws IOException {
+    HTTPAnnounceResponseMessage announceResponse;
+    try {
+      announceResponse = HTTPAnnounceResponseMessage.craft(
+              isSeeder ? SEEDER_ANNOUNCE_INTERVAL : myAnnounceInterval,
+              torrent.seeders(),
+              torrent.leechers(),
+              isSeeder ? Collections.<Peer>emptyList() : torrent.getSomePeers(peer),
+              torrent.getHexInfoHash());
+      requestHandler.serveResponse(Status.OK.getCode(), Status.OK.getDescription(), announceResponse.getData());
+    } catch (Exception e) {
+      serveError(Status.INTERNAL_SERVER_ERROR, e.getMessage(), requestHandler);
+    }
+  }
+
+  /**
+   * Parse the query parameters using our defined BYTE_ENCODING.
+   * <p>
+   * <p>
+   * Because we're expecting byte-encoded strings as query parameters, we
+   * can't rely on SimpleHTTP's QueryParser which uses the wrong encoding for
+   * the job and returns us unparsable byte data. We thus have to implement
+   * our own little parsing method that uses BYTE_ENCODING to decode
+   * parameters from the URI.
+   * </p>
+   * <p>
+   * <p>
+   * <b>Note:</b> array parameters are not supported. If a key is present
+   * multiple times in the URI, the latest value prevails. We don't really
+   * need to implement this functionality as this never happens in the
+   * Tracker HTTP protocol.
+   * </p>
+   *
+   * @param uri
+   * @param hostAddress
+   * @return The {@link AnnounceRequestMessage} representing the client's
+   * announce request.
+   */
+  // 根据客户端传来uri,解析数据,生成一个HTTPAnnounceRequestMessage对象
+  private HTTPAnnounceRequestMessage parseQuery(final String uri, final String hostAddress)
+          throws IOException, MessageValidationException {
+    Map<String, BEValue> params = new HashMap<String, BEValue>(); //客户端传来的参数
+
+    try {
+//			String uri = request.getAddress().toString();
+      for (String pair : uri.split("[?]")[1].split("&")) {
+        String[] keyval = pair.split("[=]", 2);
+        if (keyval.length == 1) {
+          this.recordParam(params, keyval[0], null);
+        } else {
+          this.recordParam(params, keyval[0], keyval[1]);
+        }
+      }
+    } catch (ArrayIndexOutOfBoundsException e) {
+      params.clear();
+    }
+
+    // Make sure we have the peer IP, fallbacking on the request's source
+    // address if the peer didn't provide it.
+    if (params.get("ip") == null) {
+      params.put("ip", new BEValue(
+              hostAddress,
+              Constants.BYTE_ENCODING));
+    }
+
+    return HTTPAnnounceRequestMessage.parse(new BEValue(params));
+  }
+
+  private void recordParam(Map<String, BEValue> params, String key, String value) {
+    try {
+      value = URLDecoder.decode(value, Constants.BYTE_ENCODING);
+
+      for (String f : NUMERIC_REQUEST_FIELDS) {
+        if (f.equals(key)) {
+          params.put(key, new BEValue(Long.valueOf(value)));
+          return;
+        }
+      }
+
+      params.put(key, new BEValue(value, Constants.BYTE_ENCODING));
+    } catch (UnsupportedEncodingException uee) {
+      // Ignore, act like parameter was not there
+    }
+  }
+
+  /**
+   * Write a {@link HTTPTrackerErrorMessage} to the response with the given
+   * HTTP status code.
+   *
+   * @param status The HTTP status code to return.
+   * @param error  The error reported by the tracker.
+   */
+  private void serveError(Status status, HTTPTrackerErrorMessage error, RequestHandler requestHandler) throws IOException {
+    requestHandler.serveResponse(status.getCode(), status.getDescription(), error.getData());
+  }
+
+  /**
+   * Write an error message to the response with the given HTTP status code.
+   *
+   * @param status The HTTP status code to return.
+   * @param error  The error message reported by the tracker.
+   */
+  private void serveError(Status status, String error, RequestHandler requestHandler) throws IOException {
+    this.serveError(status, HTTPTrackerErrorMessage.craft(error), requestHandler);
+  }
+
+  /**
+   * Write a tracker failure reason code to the response with the given HTTP
+   * status code.
+   *
+   * @param status The HTTP status code to return.
+   * @param reason The failure reason reported by the tracker.
+   */
+  private void serveError(Status status, ErrorMessage.FailureReason reason, RequestHandler requestHandler) throws IOException {
+    this.serveError(status, reason.getMessage(), requestHandler);
+  }
+
+  public void setAcceptForeignTorrents(boolean acceptForeignTorrents) {
+    myAcceptForeignTorrents = acceptForeignTorrents;
+  }
+
+  public interface RequestHandler {
+    // 返回http响应
+    void serveResponse(int code, String description, ByteBuffer responseData);
+  }
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerServiceContainer.java b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerServiceContainer.java
new file mode 100644
index 0000000..6fcecca
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/main/java/com/turn/ttorrent/tracker/TrackerServiceContainer.java
@@ -0,0 +1,113 @@
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.common.LoggerUtils;
+import com.turn.ttorrent.common.TorrentLoggerFactory;
+import org.apache.commons.io.IOUtils;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+import org.simpleframework.http.core.Container;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * @author Sergey.Pak
+ * Date: 8/12/13
+ * Time: 8:25 PM
+ */
+
+// http请求处理器
+public class TrackerServiceContainer implements Container {
+
+  private static final Logger logger =
+          TorrentLoggerFactory.getLogger(TrackerServiceContainer.class);
+
+  private TrackerRequestProcessor myRequestProcessor;
+  private final MultiAnnounceRequestProcessor myMultiAnnounceRequestProcessor;
+
+  public TrackerServiceContainer(final TrackerRequestProcessor requestProcessor,
+                                 final MultiAnnounceRequestProcessor multiAnnounceRequestProcessor) {
+    myRequestProcessor = requestProcessor;
+    myMultiAnnounceRequestProcessor = multiAnnounceRequestProcessor;
+  }
+
+  /**
+   * Handle the incoming request on the tracker service.
+   * <p/>
+   * <p>
+   * This makes sure the request is made to the tracker's announce URL, and
+   * delegates handling of the request to the <em>process()</em> method after
+   * preparing the response object.
+   * </p>
+   *
+   * @param request  The incoming HTTP request.
+   * @param response The response object.
+   */
+
+  // 处理单个的http请求 或是多个请求
+  @Override
+  public void handle(Request request, final Response response) {
+    // Reject non-announce requests
+    if (!Tracker.ANNOUNCE_URL.equals(request.getPath().toString())) {
+      response.setCode(404);
+      response.setText("Not Found");
+      return;
+    }
+
+    OutputStream body = null;
+    try {
+      body = response.getOutputStream();
+
+      response.set("Content-Type", "text/plain");
+      response.set("Server", "");
+      response.setDate("Date", System.currentTimeMillis());
+
+      if ("GET".equalsIgnoreCase(request.getMethod())) {//单独请求
+
+        myRequestProcessor.process(request.getAddress().toString(), request.getClientAddress().getAddress().getHostAddress(),
+                getRequestHandler(response));
+      } else {//多请求处理
+        myMultiAnnounceRequestProcessor.process(request.getContent(), request.getAddress().toString(),
+                request.getClientAddress().getAddress().getHostAddress(), getRequestHandler(response));
+      }
+      body.flush();
+    } catch (IOException ioe) {
+      logger.info("Error while writing response: {}!", ioe.getMessage());
+    } catch (Throwable t) {
+      LoggerUtils.errorAndDebugDetails(logger, "error in processing request {}", request, t);
+    } finally {
+      IOUtils.closeQuietly(body);
+    }
+
+  }
+
+  private TrackerRequestProcessor.RequestHandler getRequestHandler(final Response response) {
+    return new TrackerRequestProcessor.RequestHandler() {
+      @Override
+      public void serveResponse(int code, String description, ByteBuffer responseData) {
+        response.setCode(code);
+        response.setText(description);
+        try {
+          responseData.rewind();
+          final WritableByteChannel channel = Channels.newChannel(response.getOutputStream());
+          channel.write(responseData);
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+      }
+    };
+  }
+
+  public void setAcceptForeignTorrents(boolean acceptForeignTorrents) {
+    myRequestProcessor.setAcceptForeignTorrents(acceptForeignTorrents);
+  }
+
+  public void setAnnounceInterval(int announceInterval) {
+    myRequestProcessor.setAnnounceInterval(announceInterval);
+  }
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/test/java/com/turn/ttorrent/tracker/MultiAnnounceRequestProcessorTest.java b/ttorrent-master/ttorrent-tracker/src/test/java/com/turn/ttorrent/tracker/MultiAnnounceRequestProcessorTest.java
new file mode 100644
index 0000000..3d7d216
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/test/java/com/turn/ttorrent/tracker/MultiAnnounceRequestProcessorTest.java
@@ -0,0 +1,108 @@
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.TempFiles;
+import com.turn.ttorrent.Utils;
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPAnnounceResponseMessage;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+@Test
+public class MultiAnnounceRequestProcessorTest {
+
+  private Tracker tracker;
+  private TempFiles tempFiles;
+
+
+  public MultiAnnounceRequestProcessorTest() {
+    if (Logger.getRootLogger().getAllAppenders().hasMoreElements())
+      return;
+    BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("[%d{MMdd HH:mm:ss,SSS}] %6p - %20.20c - %m %n")));
+    Logger.getRootLogger().setLevel(Utils.getLogLevel());
+  }
+
+  @BeforeMethod
+  protected void setUp() throws Exception {
+    tempFiles = new TempFiles();
+    startTracker();
+  }
+
+  public void processCorrectTest() throws TrackerMessage.MessageValidationException, IOException {
+    final URL url = new URL("http://localhost:6969/announce");
+
+    final String urlTemplate = url.toString() +
+            "?info_hash={hash}" +
+            "&peer_id=ABCDEFGHIJKLMNOPQRST" +
+            "&ip={ip}" +
+            "&port={port}" +
+            "&downloaded=1234" +
+            "&left=0" +
+            "&event=started";
+
+    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+    StringBuilder requestString = new StringBuilder();
+    for (int i = 0; i < 5; i++) {
+      if (i != 0) {
+        requestString.append("\n");
+      }
+      requestString.append(getUrlFromTemplate(urlTemplate, "1" + i, "127.0.0.1", 6881));
+    }
+    connection.setRequestMethod("POST");
+    connection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8");
+    connection.setDoOutput(true);
+    connection.getOutputStream().write(requestString.toString().getBytes("UTF-8"));
+
+    final InputStream inputStream = connection.getInputStream();
+
+    final BEValue bdecode = BDecoder.bdecode(inputStream);
+
+    assertEquals(tracker.getTrackedTorrents().size(), 5);
+    assertEquals(bdecode.getList().size(), 5);
+
+    for (BEValue beValue : bdecode.getList()) {
+
+      final HTTPAnnounceResponseMessage responseMessage = HTTPAnnounceResponseMessage.parse(beValue);
+      assertTrue(responseMessage.getPeers().isEmpty());
+      assertEquals(1, responseMessage.getComplete());
+      assertEquals(0, responseMessage.getIncomplete());
+    }
+  }
+
+  private String getUrlFromTemplate(String template, String hash, String ip, int port) {
+    return template.replace("{hash}", hash).replace("{ip}", ip).replace("{port}", String.valueOf(port));
+  }
+
+
+  private void startTracker() throws IOException {
+    this.tracker = new Tracker(6969);
+    tracker.setAnnounceInterval(5);
+    tracker.setPeerCollectorExpireTimeout(10);
+    this.tracker.start(true);
+  }
+
+  private void stopTracker() {
+    this.tracker.stop();
+  }
+
+  @AfterMethod
+  protected void tearDown() throws Exception {
+    stopTracker();
+    tempFiles.cleanup();
+  }
+
+}
diff --git a/ttorrent-master/ttorrent-tracker/src/test/java/com/turn/ttorrent/tracker/TorrentsRepositoryTest.java b/ttorrent-master/ttorrent-tracker/src/test/java/com/turn/ttorrent/tracker/TorrentsRepositoryTest.java
new file mode 100644
index 0000000..5f5b842
--- /dev/null
+++ b/ttorrent-master/ttorrent-tracker/src/test/java/com/turn/ttorrent/tracker/TorrentsRepositoryTest.java
@@ -0,0 +1,168 @@
+package com.turn.ttorrent.tracker;
+
+import com.turn.ttorrent.MockTimeService;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.testng.Assert.*;
+
+@Test
+public class TorrentsRepositoryTest {
+
+  private TorrentsRepository myTorrentsRepository;
+
+  @BeforeMethod
+  public void setUp() throws Exception {
+    myTorrentsRepository = new TorrentsRepository(10);
+  }
+
+  @AfterMethod
+  public void tearDown() throws Exception {
+
+  }
+
+  public void testThatTorrentsStoredInRepository() {
+    assertEquals(myTorrentsRepository.getTorrents().size(), 0);
+    final TrackedTorrent torrent = new TrackedTorrent(new byte[]{1, 2, 3});
+
+    myTorrentsRepository.putIfAbsent(torrent.getHexInfoHash(), torrent);
+    assertTrue(myTorrentsRepository.getTorrent(torrent.getHexInfoHash()) == torrent);
+    final TrackedTorrent torrentCopy = new TrackedTorrent(new byte[]{1, 2, 3});
+
+    myTorrentsRepository.putIfAbsent(torrentCopy.getHexInfoHash(), torrentCopy);
+    assertTrue(myTorrentsRepository.getTorrent(torrent.getHexInfoHash()) == torrent);
+    assertEquals(myTorrentsRepository.getTorrents().size(), 1);
+
+    final TrackedTorrent secondTorrent = new TrackedTorrent(new byte[]{3, 2, 1});
+    myTorrentsRepository.putIfAbsent(secondTorrent.getHexInfoHash(), secondTorrent);
+    assertEquals(myTorrentsRepository.getTorrents().size(), 2);
+  }
+
+  public void testPutIfAbsentAndUpdate() throws UnsupportedEncodingException {
+
+    final AtomicBoolean updateInvoked = new AtomicBoolean();
+    TrackedTorrent torrent = new TrackedTorrent(new byte[]{1, 2, 3}) {
+      @Override
+      public TrackedPeer update(AnnounceRequestMessage.RequestEvent event, ByteBuffer peerId, String hexPeerId, String ip, int port, long uploaded, long downloaded, long left) throws UnsupportedEncodingException {
+        updateInvoked.set(true);
+        return super.update(event, peerId, hexPeerId, ip, port, uploaded, downloaded, left);
+      }
+    };
+    myTorrentsRepository.putIfAbsentAndUpdate(torrent.getHexInfoHash(), torrent,
+            AnnounceRequestMessage.RequestEvent.STARTED, ByteBuffer.allocate(5), "0",
+            "127.0.0.1", 6881, 5, 10, 12);
+    assertTrue(updateInvoked.get());
+    assertEquals(torrent.getPeers().size(), 1);
+    final TrackedPeer trackedPeer = torrent.getPeers().values().iterator().next();
+    assertEquals(trackedPeer.getIp(), "127.0.0.1");
+    assertEquals(trackedPeer.getPort(), 6881);
+    assertEquals(trackedPeer.getLeft(), 12);
+    assertEquals(trackedPeer.getDownloaded(), 10);
+    assertEquals(trackedPeer.getUploaded(), 5);
+  }
+
+  public void testThatCleanupDontLockAllTorrentsAndStorage() throws UnsupportedEncodingException {
+
+    final Semaphore cleanFinishLock = new Semaphore(0);
+    final Semaphore cleanStartLock = new Semaphore(0);
+    final TrackedTorrent torrent = new TrackedTorrent(new byte[]{1, 2, 3}) {
+      @Override
+      public void collectUnfreshPeers(int expireTimeoutSec) {
+        cleanStartLock.release();
+        try {
+          if (!cleanFinishLock.tryAcquire(1, TimeUnit.SECONDS)) {
+            fail("can not acquire semaphore");
+          }
+        } catch (InterruptedException e) {
+          fail("can not finish cleanup", e);
+        }
+      }
+    };
+
+    myTorrentsRepository.putIfAbsent(torrent.getHexInfoHash(), torrent);
+    torrent.addPeer(new TrackedPeer(torrent, "127.0.0.1", 6881, ByteBuffer.allocate(10)));
+    assertEquals(myTorrentsRepository.getTorrents().size(), 1);
+
+    final ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+    try {
+      final Future<Integer> cleanupFuture = executorService.submit(new Callable<Integer>() {
+        @Override
+        public Integer call() throws Exception {
+          myTorrentsRepository.cleanup(1);
+          return 0;
+        }
+      });
+      try {
+        if (!cleanStartLock.tryAcquire(1, TimeUnit.SECONDS)) {
+          fail("cannot acquire semaphore");
+        }
+      } catch (InterruptedException e) {
+        fail("don't received that cleanup is started", e);
+      }
+
+      final TrackedTorrent secondTorrent = new TrackedTorrent(new byte[]{3, 1, 1});
+
+      myTorrentsRepository.putIfAbsentAndUpdate(secondTorrent.getHexInfoHash(), secondTorrent,
+              AnnounceRequestMessage.RequestEvent.STARTED, ByteBuffer.allocate(5), "0",
+              "127.0.0.1", 6881, 0, 0, 1);
+
+      cleanFinishLock.release();
+      try {
+        cleanupFuture.get(1, TimeUnit.SECONDS);
+      } catch (InterruptedException e) {
+        fail("cleanup was interrupted", e);
+      } catch (ExecutionException e) {
+        fail("cleanup was failed with execution exception", e);
+      } catch (TimeoutException e) {
+        fail("cannot get result from future", e);
+      }
+    } finally {
+      executorService.shutdown();
+    }
+  }
+
+  public void testThatTorrentsCanRemovedFromStorage() throws UnsupportedEncodingException {
+    TrackedTorrent torrent = new TrackedTorrent(new byte[]{1, 2, 3});
+
+    MockTimeService timeService = new MockTimeService();
+    timeService.setTime(10000);
+    final TrackedPeer peer = new TrackedPeer(torrent, "127.0.0.1", 6881, ByteBuffer.allocate(5), timeService);
+    torrent.addPeer(peer);
+
+    timeService.setTime(15000);
+    final TrackedPeer secondPeer = new TrackedPeer(torrent, "127.0.0.1", 6882, ByteBuffer.allocate(5), timeService);
+    torrent.addPeer(secondPeer);
+
+    myTorrentsRepository.putIfAbsent(torrent.getHexInfoHash(), torrent);
+
+    assertEquals(myTorrentsRepository.getTorrents().size(), 1);
+    assertEquals(torrent.getPeers().size(), 2);
+
+    timeService.setTime(17000);
+    myTorrentsRepository.cleanup(10);
+
+    assertEquals(myTorrentsRepository.getTorrents().size(), 1);
+    assertEquals(torrent.getPeers().size(), 2);
+
+    timeService.setTime(23000);
+    myTorrentsRepository.cleanup(10);
+
+    assertEquals(myTorrentsRepository.getTorrents().size(), 1);
+    assertEquals(torrent.getPeers().size(), 1);
+
+    timeService.setTime(40000);
+    myTorrentsRepository.cleanup(10);
+
+    assertEquals(myTorrentsRepository.getTorrents().size(), 0);
+    assertEquals(torrent.getPeers().size(), 0);
+
+  }
+}