// java分片下载文件
package com.example.demo.Utils;


import cn.hutool.core.date.SystemClock;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Description 文件下载辅助类,支持分段并发下载,支持断点下载
 * @Author Chen
 * @Date 2022/05/27 15:56
 */
@Slf4j
public class FileDownloadHelper {
    /**
     * 文件元数据
     */
    private FileMetaData fileMetaData;
    /**
     * 请求方式
     */
    private String way;

    /**
     * 连接超时时长:ms
     */
    private int connectTimeout;
    /**
     * 读取数据超时时长:ms
     */
    private int readTimeout;
    /**
     * 分片数目
     */
    private int splitCount;
    /**
     * 完整数据的连接对象
     */
    private HttpURLConnection connection;

    private FileSplitFetchTask[] fetchTasks;

    private FileDownloadHelper() {
    }

    public static Builder builder() {
        return new Builder();
    }

    private long getLength() {
        try {

            if (fileMetaData.length != -1) {
                return fileMetaData.length;
            }
            HttpURLConnection connection = getConnection();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                long length = Long.parseLong(connection.getHeaderField("Content-Length"));
                this.fileMetaData.length = length;
                return length;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return -1;
    }

    private HttpURLConnection getConnection() {
        try {
            if (connection == null) {
                HttpURLConnection connection = (HttpURLConnection) new URL(fileMetaData.url).openConnection();
                connection.setReadTimeout(readTimeout);
                connection.setConnectTimeout(connectTimeout);
                connection.setRequestMethod(way);

                // 设置请求头
                if (Builder.headers != null && Builder.headers.size() > 0) {
                    for (String key : Builder.headers.keySet()) {
                        String value = Builder.headers.get(key);
                        connection.setRequestProperty(key, value);
                    }
                }

                this.connection = connection;
                connection.disconnect();
                return connection;
            }
        } catch (Exception e) {
            log.error("获取连接时发生异常:{}", e.getMessage());
        }
        return connection;
    }

    private HttpURLConnection getSplitConnection(long startPos, long endPos) {

        HttpURLConnection connections = null;
        try {
            connections = (HttpURLConnection) new URL(fileMetaData.url).openConnection();
            connections.setReadTimeout(readTimeout);
            connections.setConnectTimeout(connectTimeout);
            connections.setRequestMethod(way);
            String prop = "bytes=" + startPos + "-" + endPos;
            log.info("分片参数:{}", prop);

            // 设置请求头
            if (Builder.headers != null && Builder.headers.size() > 0) {
                for (String key : Builder.headers.keySet()) {
                    String value = Builder.headers.get(key);
                    connections.setRequestProperty(key, value);
                }
            }

            connections.setRequestProperty("RANGE", prop);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return connections;
    }

    private InputStream getInputStreamFromConnection(HttpURLConnection connection) {
        try {
            return connection.getInputStream();
        } catch (IOException e) {
            log.error("获取输入流时发生异常:{}", e.getMessage());
        }
        return null;
    }

    public FileMetaData getFileMetaData() {
        return fileMetaData;
    }


    private FileSplitFetchTask[] createFileSplitFetchTask() {
        FileSplitFetchTask[] fetchTasks = new FileSplitFetchTask[splitCount];
        long length = getLength();

        if (length == -1) {
            log.error("文件大小未知,服务创建分片任务失败");
            return fetchTasks;
        } else if (length == -2) {
            log.error("文件:{}不存在", fileMetaData.name);
            return fetchTasks;
        }
        int lastEndPos = 0;
        int startPos = 0;
        int endPos = 0;
        double averageLength = (length * 1.0) / splitCount;
        for (int i = 0; i < splitCount; i++) {
            if (lastEndPos != 0) {
                startPos = lastEndPos + 1;
            }
            endPos = startPos + (int) Math.ceil(averageLength);
            fetchTasks[i] = new FileSplitFetchTask(startPos, endPos, i);
            lastEndPos = endPos;
        }
        return fetchTasks;
    }


    public InputStream getInputStream() {
        return fileMetaData.getInputStream();
    }


    public boolean write() {
        try {
            if (fileMetaData.content != null && fileMetaData.content.size() > 0 && !StringUtils.isEmpty(fileMetaData.path)) {
                File file = new File(fileMetaData.path + File.separator + fileMetaData.name);
                FileOutputStream outputStream = new FileOutputStream(file);
                outputStream.write(fileMetaData.getContentAsByteArray());
                outputStream.flush();
                outputStream.close();
                return true;

            } else {
                log.error("没有指定写入路径,写入失败");
                return false;
            }
        } catch (Exception e) {
            log.error("写入时发生异常:{}", e.getMessage());
            e.printStackTrace();
        }
        return false;
    }

    public void stopDownload() {
        if (this.fetchTasks != null) {
            for (FileSplitFetchTask task : fetchTasks) {
                task.stop();
            }
        }
    }

    public void continueDownload() {
        if (this.fetchTasks != null) {
            for (FileSplitFetchTask task : fetchTasks) {
                task.goOn();
            }
        }
    }

    public void download() {
        int totalCount = 0;
        try {
            //构建分片任务对象
            FileSplitFetchTask[] fetchTasks = createFileSplitFetchTask();
            this.fetchTasks = fetchTasks;
            List<FutureTask<Integer>> futureTasks = new ArrayList<>();
            for (int i = 0; i < fetchTasks.length; i++) {
                FutureTask<Integer> futureTask = new FutureTask<>(fetchTasks[i]);
                futureTasks.add(futureTask);
                Thread thread = new Thread(futureTask);
                thread.setName(String.valueOf(i));
                //启动下载
                thread.start();
            }

            //阻塞等待所有线程下载完
            for (FutureTask<Integer> future : futureTasks) {
                totalCount += future.get();
            }
            //拼接内容
            for (FileSplitFetchTask task : fetchTasks) {
                byte[] cache = task.getContent();
                assert cache != null;
                for (byte b : cache) {
                    fileMetaData.content.add(b);
                }
            }
            //写出
            if (!StringUtils.isEmpty(fileMetaData.path) && write()) {
                log.info("写出成功,路径:{}", fileMetaData.path + File.separator + fileMetaData.name);
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("下载过程发生异常:{}", e.getMessage());
            return;
        }
        if (totalCount != fileMetaData.length) {
            log.warn("下载字节数:{}与实际字节数:{}不匹配", totalCount, fileMetaData.length);
        } else {
            //log.info("下载成功");
        }

    }

    public void clear() {
        this.fileMetaData = null;
        connection.disconnect();
        connection = null;
        System.gc();
    }

    /**
     * 下载文件的元数据
     */
    @Setter(AccessLevel.PRIVATE)
    private static class FileMetaData {
        /**
         * 地址
         */
        private String url;
        /**
         * 长度
         */
        private long length = -1;
        /**
         * 写入路径
         */
        private String path;
        /**
         * 文件名,包含后缀
         */
        private String name;
        /**
         * 后缀,不包含.
         */
        private String suffix;
        /**
         * 内容,二进制字列表
         */
        private List<Byte> content;

        private byte[] arrayContent;
        /**
         * 文件的输入流
         */
        private InputStream inputStream;

        public byte[] getContentAsByteArray() {
            if (arrayContent != null) {
                return arrayContent;
            }
            if (content != null) {
                int i = 0;
                byte[] holder = new byte[content.size()];
                for (Byte bt : content) {
                    holder[i++] = bt;
                }
                arrayContent = holder;
                return holder;

            }
            return new byte[]{};
        }

        public InputStream getInputStream() {
            if (inputStream == null) {
                inputStream = new ByteArrayInputStream(getContentAsByteArray());
            }
            return inputStream;
        }
    }

    @Setter
    @Accessors(chain = true)
    public static class Builder {
        /**
         * 地址
         */
        private String url;
        /**
         * 写入路径
         */
        private String path;
        /**
         * 文件名,包含后缀
         */
        private String name;
        /**
         * 请求头
         */
        private static HashMap<String, String> headers;
        /**
         * 请求方式
         */
        private String way = "get";
        /**
         * 连接超时时长:ms
         */
        private int connectTimeout = 5000;
        /**
         * 读取数据超时时长:ms
         */
        private int readTimeout = 5000;
        /**
         * 是否允许分片下载
         */
        private boolean allowSplitDownload = true;
        /**
         * 分片数目
         */
        private int splitCount = 3;

        public Builder setConnectTimeout(int connectTimeout, TimeUnit unit) {
            this.connectTimeout = (int) unit.toMillis(connectTimeout);
            return this;
        }

        public Builder setReadTimeout(int readTimeout, TimeUnit unit) {
            this.readTimeout = (int) unit.toMillis(readTimeout);
            return this;
        }

        public FileDownloadHelper build() {
            if (!check()) {
                throw new IllegalArgumentException("错误参数,请检查");
            }
            FileDownloadHelper fileDownloadHelper = new FileDownloadHelper();
            FileMetaData fileMetaData = new FileMetaData();
            if (StringUtils.isEmpty(name)) {
                int namePos = url.trim().lastIndexOf("/");
                name = url.substring(namePos + 1);
            }
            fileMetaData.setName(name);
            int pos = fileMetaData.name.lastIndexOf(".");
            if (pos != -1) {
                fileMetaData.setSuffix(fileMetaData.name.substring(pos));
            }
            fileMetaData.setPath(path);
            fileMetaData.setUrl(url.trim());
            fileMetaData.content = new ArrayList<>();
            if (way.trim().equalsIgnoreCase("get") || way.trim().equalsIgnoreCase("post")) {
                fileDownloadHelper.way = way.trim().toUpperCase();
            }
            fileDownloadHelper.connectTimeout = connectTimeout;
            fileDownloadHelper.readTimeout = readTimeout;
            fileDownloadHelper.fileMetaData = fileMetaData;
            fileDownloadHelper.splitCount = allowSplitDownload ? splitCount : 1;
            return fileDownloadHelper;
        }

        private boolean check() {
            boolean flag = url.lastIndexOf("/") != -1 || url.lastIndexOf(File.separator) != -1;
            return !StringUtils.isEmpty(url) && flag && splitCount > 0;
        }

        public Builder setHeaders(HashMap<String, String> map) {
            headers = map;
            return this;
        }
    }

    private class FileSplitFetchTask implements Callable<Integer> {
        /**
         * 开始索引
         */
        private final long startPos;
        /**
         * 终止索引
         */
        private final long endPos;
        /**
         * 开始标志
         */
        private boolean isStart = false;
        /**
         * 结束标志
         */
        private boolean isOver = false;
        /**
         * 暂停标志
         */
        private AtomicBoolean isStop = new AtomicBoolean(false);
        /**
         * 线程号
         */
        private final int threadId;
        /**
         * 内容
         */
        private final byte[] content;
        /**
         * 分片请求
         */
        private final HttpURLConnection splitConnection;

        public FileSplitFetchTask(long startPos, long endPos, int threadId) {
            if (endPos < startPos) {
                throw new IllegalArgumentException("终止索引不得小于起始索引");
            }
            this.startPos = startPos;
            this.endPos = endPos;
            this.threadId = threadId;
            this.content = new byte[(int) (endPos - startPos + 1)];
            this.splitConnection = getSplitConnection(startPos, endPos);
        }

        public byte[] getContent() {
            if (isOver) {
                return content;
            } else {
                log.error("线程:{}正在拉取,无法获取", threadId);
                return null;
            }
        }

        public void stop() {
            log.info("线程:{}已暂停下载", threadId);
            this.isStop.set(true);
        }

        public void goOn() {
            log.info("线程:{}已继续下载", threadId);
            this.isStop.set(false);
        }

        public boolean isStart() {
            return isStart;
        }

        public boolean isOver() {
            return isOver;
        }

        public boolean isStop() {
            return isStop.get();
        }

        @Override
        public Integer call() throws Exception {
            long start = SystemClock.now();
            log.info("线程:{}下载开始", threadId);
            InputStream inputStream = getInputStreamFromConnection(splitConnection);
            if (inputStream == null) {
                throw new NullPointerException("线程:" + threadId + "下载失败,输入流为空");
            }
            int cache;
            try {
                isStart = true;
                int pos = 0;
                while (!isStop.get()) {
                    cache = inputStream.read();
                    if (cache != -1) {
                        content[pos++] = (byte) cache;
                    } else {
                        break;
                    }
                }
                log.info("线程:{}已下载完,共计:{}字节,共计用时:{}ms", threadId, pos, SystemClock.now() - start);
                isStop.set(true);
                isOver = true;
                inputStream.close();
                return pos;
            } catch (IOException e) {
                e.printStackTrace();
            }
            return 0;
        }
    }


}



// 使用
// 分片下载
        long start = SystemClock.now();
        String[] urls = new String[]{"https://avatar-1309914555.cos.ap-chengdu.myqcloud.com/UU-4.27.0.exe"};
        for (String url : urls) {
            // 请求头
            HashMap<String, String> map = new HashMap<>();
            map.put("222", "111");
            map.put("1111", "222");
            FileDownloadHelper downloadHelper = FileDownloadHelper
                    .builder()
                    .setUrl(url)
                    .setWay("get")
                    .setAllowSplitDownload(true)
                    .setSplitCount(10)
                    .setConnectTimeout(5, TimeUnit.SECONDS)
                    .setReadTimeout(60, TimeUnit.SECONDS)
                    .setPath("D://").setName("UU.exe")
                    .setHeaders(map)
                    .build();
            downloadHelper.download();
        }
        System.out.println("下载完成,总计用时:" + (SystemClock.now() - start) + "ms");