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 @@
+[](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 "locator" 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 "locator" 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 "locator" 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 "locator" parameter instead. Note that corresponding locator dimension is "vcsRootInstance"</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 "locator" 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 "locator" 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 "locator" 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 "/{buildLocator}/artifacts/children" 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);
+
+ }
+}