public class JIRA {
    //https://docs.atlassian.com/jira-software/REST/cloud/
    //https://docs.atlassian.com/jira-software/REST/
    //JQL : https://confluence.atlassian.com/jirasoftwarecloud/advanced-searching-764478330.html
    //JQL Fields : https://confluence.atlassian.com/jirasoftwarecloud/advanced-searching-fields-reference-764478339.html
    //该api默认最大返回50条记录，所以maxResult不能大于50，且只能分页查询。
    public static final String AGILE_BOARDS_URL  = "/rest/agile/1.0/board?type=scrum&startAt={start}&maxResults={max}";
    public static final String AGILE_BOARD_PROJECT_URL = "/rest/agile/1.0/board/{boardId}/project?startAt={start}&&maxResults={max}";
    public static final String AGILE_SPRINTS_URL = "/rest/agile/1.0/board/{boardId}/sprint?startAt={start}&maxResults={max}";
    public static final String AGILE_EPICS_URL = "/rest/agile/1.0/board/{boardId}/epic?startAt={start}&maxResults={max}";
    public static final String AGILE_ISSUES_IN_EPIC_URL = "/rest/agile/1.0/board/{boardId}/epic/{epicId}/issue?startAt={start}&maxResults={max}";
    public static final String AGILE_BACKLOG_IN_BOARD_URL = "/rest/agile/1.0/board/{boardId}/backlog?startAt={start}&maxResults={max}";

    public static final String AGILE_ISSUES_URL  = "/rest/agile/1.0/board/{boardId}/sprint/{sprintId}/issue?startAt={start}&maxResults={max}";
    public static final String GREENHOPPER_ISSUES_URL  = "/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId={boardId}&activeSprints={sprintId}";
    public static final String GREENHOPPER_BACKLOG_URL  = "/rest/greenhopper/1.0/xboard/plan/backlog/data.json?rapidViewId={boardId}";
    public static final String AGILE_ISSUE_URL   = "/rest/agile/1.0/issue/{issueId}";
    public static final String BROWSE_ISSUE_URL   = "/browse/{issueKey}";
    public static final String BROWSE_BOARD_URL = "/secure/RapidBoard.jspa?projectKey={projectKey}&view=planning.nodetail";
    public static final String BROWSE_SPRINT_URL = "/secure/RapidBoard.jspa?projectKey={projectKey}&view=planning";

    //http://jira.xiaoquer.cc/rest/greenhopper/1.0/xboard/plan/backlog/data.json?rapidViewId=232&selectedProjectKey=CPC
//    public static final String GREENHOPPER_BACKLOG_URL = "";

    //https://confluence.atlassian.com/jiracoreserver073/advanced-searching-861257209.html#Advancedsearching-reference
    //https://developer.atlassian.com/static/rest/jira/6.1.html#d2e4071
    //*all,-comment,-watches表示查询所有属性除了comment waches
    //如此搜索用于缓存数据，不用一条一条的查询issue了
    public static final String SEARCH_BY_JQL_URL = "/rest/api/2/search?startAt={start}&maxResults=50&fields=*all%2c-comment%2c-watches&jql={jql}";

    //http://jira/rest/greenhopper/1.0/xboard/plan/backlog/data.json?rapidViewId=97
    public static final String GREENHOPPER_KANBAN = "/rest/greenhopper/1.0/xboard/plan/backlog/data.json?rapidViewId={boardId}";

    public static boolean isConnected = false;
    public static String  connectStatus = "";
    public static String  serverUrl;

    private static Realm  realm;
    private static final  int PAGE_SIZE            = 50;

    /**
     * Key=BoardId, Value=JiraBoard
     */
    public static Map<String, JiraBoard> ALL_BOARD_MAP = new TreeMap<>();

    public static Map<String, Map<String, JiraProject>> BOARD_PROJECT_MAP = new LinkedHashMap<>();

//    private static Vector<String> boardNames;
    /**
     * Key=BoardId, Value=Map<String, JiraSprint>
     */
    public static Map<String, Map<String, JiraSprint>> BOARD_SPRINT_MAP = new LinkedHashMap<>();

    /**
     * Key=SprintId, Value=JiraSprint
     */
    public static Map<String, JiraSprint> ALL_SPRINT_MAP = new TreeMap<>();

    /**
     * Key=BoardId+","+SprintId, value= 只包含Story或者任务
     */
    public static Map<String, Map<String, JiraIssue>> BOARD_SPRINT_PARENT_MAP = new LinkedHashMap<>();

    /**
     * Key=IssueId
     */
    public static Map<String, JiraIssue> ALL_ISSUE_MAP = new TreeMap<>();

    /**
     * Key=EpicId
     */
    public static final Map<String, JiraEpic> ALL_EPIC_MAP = new LinkedHashMap<>();
    /**
     * Key1=BoardId
     * Key2=EpicId
     */
    public static final Map<String, Map<String, JiraEpic>> BOARD_EPICS_MAP = new LinkedHashMap<>();

    //Key=EpicId
    public static Map<String, Map<String, JiraIssue>> EPIC_ISSUES_MAP = new LinkedHashMap<>();

//    private static Map<String, JiraSprint> sprintMap;
//    private static Vector<String> sprintNames;

    private static AsyncHttpClient asyncHttpClient;

    private JIRA() {
    }

    public static boolean connect(String url, String username, String password) {
        if (url.endsWith("/")) {
            serverUrl = url.substring(0, url.length() - 1);
        } else {
            serverUrl = url;
        }

        realm = new Realm.Builder(username, password)
                .setUsePreemptiveAuth(true)
                .setScheme(Realm.AuthScheme.BASIC)
                .build();

        if (asyncHttpClient == null) {
            asyncHttpClient = new DefaultAsyncHttpClient();
        }

        clearCache();

        try {
            //连接就获取看板列表
            getBoardMap(true);
            isConnected = true;
            connectStatus = "连接成功";
        } catch (Exception e) {
            e.printStackTrace();
            if (e instanceof JSONException) {
                if (e.getMessage().contains("Unauthorized")) {
                    connectStatus = "账号或密码错误";
                } else if (e.getMessage().contains("Forbidden")) {
                    connectStatus = "账号被锁定";
                } else {
                    connectStatus = "账号异常,联系管理员";
                }

            } else {
                Throwable t = e.getCause();
                if (t instanceof TimeoutException) {
                    connectStatus = "连接超时";
                } else if (t instanceof InterruptedException || t instanceof ExecutionException) {
                    connectStatus = "连接异常";
                } else {
                    connectStatus = "连接失败";
                }
            }
            isConnected = false;
        }

        return isConnected;
    }

    public static void disconnect() {
        serverUrl = "";
        realm = null;

        clearCache();
        try {
            asyncHttpClient.close();
        } catch (Exception e) {
        } finally {
            asyncHttpClient = null;
        }
    }
    private static void clearCache() {
        ALL_BOARD_MAP.clear();
        ALL_SPRINT_MAP.clear();
        ALL_ISSUE_MAP.clear();
        BOARD_SPRINT_MAP.clear();
        BOARD_SPRINT_PARENT_MAP.clear();
        ALL_EPIC_MAP.clear();
        BOARD_EPICS_MAP.clear();
        EPIC_ISSUES_MAP.clear();
    }

    public static Map<String, JiraBoard> getBoardMap(boolean forceRefresh) {
        if (forceRefresh) {
            for (int i = 0; ; i = i + PAGE_SIZE) {
                String responseBody = getResponse(serverUrl + AGILE_BOARDS_URL
                        .replace("{start}", String.valueOf(i))
                        .replace("{max}", String.valueOf(PAGE_SIZE)));
                JiraBoard.toMap(responseBody, null);

                boolean isLast = JSON.parseObject(responseBody).getBoolean("isLast");
                if (isLast) break;
            }
        }
        return ALL_BOARD_MAP;
    }

    public static Map<String, JiraBoard> getBoardMap() {
        return getBoardMap(false);
    }

    public static JiraBoard getBoardCache(String boardId) {
        return ALL_BOARD_MAP.get(boardId);
    }

    public static Map<String, JiraProject> getProjectMap(String boardId) {
        Map<String, JiraProject> projectMap = BOARD_PROJECT_MAP.get(boardId);

        if (projectMap == null || projectMap.size() == 0) {
            String projectJoStr = JIRA.getResponseRelative(JIRA.AGILE_BOARD_PROJECT_URL
                    .replace("{boardId}", boardId)
                    .replace("{start}", "0")
                    .replace("{max}", "50"));
            projectMap = JiraProject.toMap(projectJoStr);

            BOARD_PROJECT_MAP.put(boardId, projectMap);
        }

        return projectMap;
    }

    public static Map<String, JiraSprint> getSprintMap(String boardId) {
//        if (BOARD_SPRINT_MAP.get(boardId) != null) {
//            return BOARD_SPRINT_MAP.get(boardId);
//        }

        for (int i = 0;; i = i + PAGE_SIZE) {
            String responseBody = getResponse(serverUrl + AGILE_SPRINTS_URL
                    .replace("{boardId}", boardId)
                    .replace("{start}", String.valueOf(i))
                    .replace("{max}", String.valueOf(PAGE_SIZE)));

            JiraSprint.toMap(responseBody, boardId);

            boolean isLast = JSON.parseObject(responseBody).getBoolean("isLast");
            if (isLast) break;
        }

        return BOARD_SPRINT_MAP.get(boardId);
    }

    public static JiraSprint getSprintCache(String sprintId) {
        return ALL_SPRINT_MAP.get(sprintId);
    }

    public static Map<String, JiraIssue> getBacklog(String boardId) {
        int issuePageSize = PAGE_SIZE * 2;

        String sprintId = "";
        String url = serverUrl + AGILE_BACKLOG_IN_BOARD_URL
                .replace("{boardId}", boardId);

        boolean isEnd = false;

        int start = 0;
        while ( !isEnd ) {
            Pair<String, Boolean> responsePair = _getPaginationResponse(url, start, issuePageSize);

            String responseBody = responsePair.getLeft();
            isEnd = responsePair.getRight();

            JiraIssue.toMap(responseBody, boardId, sprintId);

            start+=issuePageSize;
        }

        //把子任务解析到story或者任务的类属性里
        parseSubTasks(boardId, sprintId);

        return getStoryOrTaskMap(boardId, sprintId);
    }

    //Sort backlog with the same order as jira ui shows.
    public static Map<String, JiraIssue> getBacklogIdByJiraUIOrder(String boardId) {
        List<String> backlogList = new ArrayList<>();

        String responseBody = getResponse(serverUrl + GREENHOPPER_BACKLOG_URL
                .replace("{boardId}", boardId));

        JSONObject responseJO = JSON.parseObject(responseBody);
        JSONArray sprintArray = (responseJO != null? responseJO.getJSONArray("sprints") : null);

        //将已进入sprint的backlog剔除
        List<String> intoSprintBacklogList = new ArrayList<>();
        if (sprintArray != null && sprintArray.size() > 0) {
            for (int i = 0; i < sprintArray.size(); i++) {
                JSONArray issuesIds = sprintArray.getJSONObject(i).getJSONArray("issuesIds");
                intoSprintBacklogList.addAll(issuesIds.toJavaList(String.class));
            }
        }

        JSONArray issueArray = (responseJO != null? responseJO.getJSONArray("issues") : null);

        if (issueArray != null && issueArray.size() > 0) {
            for (int i = 0; i < issueArray.size(); i++) {
                JSONObject issueJO = issueArray.getJSONObject(i);
//                backlogList.add(JiraIssue._parse(issueJO, boardId, ""));

                String issueId = issueJO.getString("id");
                if (intoSprintBacklogList.contains(issueId)) continue;

                backlogList.add(issueId);
            }
        }

        Map<String, JiraIssue> result = getBacklog(boardId);
        Map<String, JiraIssue> sortedResult = new LinkedHashMap<>();

        if (result != null && result.size() > 0 && result.size() == backlogList.size()) {
            for(String backlogId : backlogList) {
                JiraIssue backlog = result.get(backlogId);
                if (backlog == null) backlog = getIssueObjById(backlogId, boardId, "");
                sortedResult.put(backlogId, backlog);
            }
        }

        result.clear();
        result.putAll(sortedResult);

        return result;
    }

    //Key=Response String
    //Value=end or not
    private static Pair<String, Boolean> _getPaginationResponse(String url, int start, int max) {
        String responseBody = getResponse(url
                .replace("{start}", String.valueOf(start))
                .replace("{max}", String.valueOf(max)));

        int total = 0;
        try {
            total = JSON.parseObject(responseBody).getInteger("total");
        } catch (Exception e) {
            System.out.println("获取总记录数失败！！！");
            e.printStackTrace();
        }

        boolean isEnd = false;
        if (start + max >= total) isEnd = true;
        return new ImmutablePair<>(responseBody, isEnd);
    }


    public static Map<String, JiraIssue> getIssueMap(String boardId, String sprintId) {

        int issuePageSize = PAGE_SIZE * 2;

        int total = _getIssuePagination(boardId, sprintId, 0, issuePageSize);

        for (int i = issuePageSize; i <= total; i = i + issuePageSize) {
            _getIssuePagination(boardId, sprintId, i, issuePageSize);
        }

        //把子任务解析到story或者任务的类属性里
        parseSubTasks(boardId, sprintId);

        return getStoryOrTaskMap(boardId, sprintId);
    }

    /**
     * 获取issue，与jira看板中看到的顺序保持一致。
     */
    public static Map<String, JiraIssue> getIssueMapWithSameOrder(String boardId, String sprintId) {
        String key = boardId + "," + sprintId;
        Map<String, JiraIssue> thisTimeIssueMap = BOARD_SPRINT_PARENT_MAP.get(key);
        if (thisTimeIssueMap == null) {
            refreshIssuesCacheInBatch(boardId, sprintId); //用分页查询的方式获取issue存入缓存，这样比by issueId查询要快
            thisTimeIssueMap = BOARD_SPRINT_PARENT_MAP.get(key);
        }

        JiraSprint jiraSprint = ALL_SPRINT_MAP.get(sprintId);

        //Open Sprint 直接用jira本身界面上的issue顺序和接口来显示
        if (jiraSprint.isActive()) {
            String responseBody = getResponseRelative(GREENHOPPER_ISSUES_URL
                    .replace("{boardId}", boardId)
                    .replace("{sprintId}", sprintId));

            return JiraIssue.toMapWithKanbanOrder(responseBody, boardId, sprintId);
        } else {
            //Close Sprint 通过将sprint内的issue刷入缓存，再组装成树结构，用于ui显示
            return JiraIssue.toMap4FromCache(thisTimeIssueMap, boardId, sprintId);
        }
    }

    public static void refreshIssuesCacheInBatch(String boardId, String sprintId) {
        JiraBoard jiraBoard = ALL_BOARD_MAP.get(boardId);

        StringBuffer jql = new StringBuffer();
        int x = 0;

        //未知问题：BoardId查询Project可能返回空
        if (jiraBoard.getProjects().size() > 0) {
            jql.append("(");
            for (JiraProject jp : jiraBoard.getProjects().values()) {
                if (x++ > 0) {
                    jql.append(" OR ");
                }
                jql.append("Project=").append(jp.getProjectKey());
            }
            jql.append(")");

            jql.append(" AND ");
        }

        jql.append(" Sprint=").append(sprintId);

        String encodedJQL = jql.toString();
        try {
            encodedJQL = URLEncoder.encode(encodedJQL, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        int total = Integer.MAX_VALUE;
        for (int i = 0; i <= total; i = i + PAGE_SIZE) {
            String responseBody = getResponseRelative(SEARCH_BY_JQL_URL
                    .replace("{jql}", encodedJQL).replace("{start}", String.valueOf(i)));

            //将issue解析进入缓存
            JiraIssue.toMap(responseBody, boardId, sprintId);

            total = JSON.parseObject(responseBody).getInteger("total");
        }
    }

    private static void queryMyOpenIssuesByJQL() {
        StringBuffer jql = new StringBuffer("assignee=currentUser() AND resolution = Unresolved AND type!=Epic AND type!=Story order by project asc, sprint asc, updated DESC");
        String encodedJQL = jql.toString();
        try {
            encodedJQL = URLEncoder.encode(encodedJQL, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        int total = Integer.MAX_VALUE;
        for (int i = 0; i <= total; i = i + PAGE_SIZE) {
            String responseBody = getResponseRelative(SEARCH_BY_JQL_URL
                    .replace("{jql}", encodedJQL).replace("{start}", String.valueOf(i)));

            total = JSON.parseObject(responseBody).getInteger("total");
        }
    }

    /**
     * 解析story或者任务的子任务，拼装到JiraIssue类属性中
     */
    public static void parseSubTasks(String boardId, String sprintId) {
        Map<String, JiraIssue> localIssueMap = JIRA.getStoryOrTaskMap(boardId, sprintId);

        for (JiraIssue storyOrTask : localIssueMap.values()) {
            JSONArray subTasksJo = storyOrTask.getSubTaskJo();

            if (storyOrTask.isParent() && subTasksJo != null && subTasksJo.size() > 0) {
                for (int j = 0; j < subTasksJo.size(); j++) {
                    String subTaskId = subTasksJo.getJSONObject(j).getString("id");
                    JiraIssue subTask = JIRA.getIssueCached(subTaskId);

                    // 通过board/sprint/查询出的issue中可能缺失某一类issue：（在ALL_ISSUE_MAP中不存在）
                    // 这种issue本来应该在board/sprint下面的，但是他可能录入错了团队，
                    // 导致通过 /board/xx/sprint/xx/issue/xx查不到，但是 /issue/xx 查得到
                    // 所以日志只要打印了 /rest/agile/1.0/issue/16917 类似的日志，说明issue属性写错了！
                    if (subTask == null) {
                        subTask = JIRA.getIssueObjById(subTaskId, boardId, sprintId);
                        ALL_ISSUE_MAP.put(subTaskId, subTask);
//                        System.out.println("【警告】Issue " + subTask.getIssueKey() + " 的属性(比如团队或其他自定义属性)可能填写错误, 请修正！");
                    }

                    Map<String, JiraIssue> subtaskMap = storyOrTask.getSubTaskMap();
                    if (subtaskMap == null) {
                        subtaskMap = new LinkedHashMap<>();
                    }

                    subtaskMap.put(subTask.getIssueId(), subTask);
                    storyOrTask.setSubTaskMap(subtaskMap);
                }
            }
        }
    }

    public static void putStoryOrTaskMap(String boardId, String sprintId, String storyOrTaskId, JiraIssue storyOrTaskObj) {
        String key = boardId + "," + sprintId;
        Map<String, JiraIssue> thisTimeIssueMap = BOARD_SPRINT_PARENT_MAP.get(key);
        if (thisTimeIssueMap == null) {
            thisTimeIssueMap = new LinkedHashMap<>();
        }
        thisTimeIssueMap.put(storyOrTaskId, storyOrTaskObj);
        BOARD_SPRINT_PARENT_MAP.put(key, thisTimeIssueMap);
    }

    public static Map<String, JiraIssue> getStoryOrTaskMap(String boardId, String sprintId) {
        String key = boardId + "," + sprintId;
        Map<String, JiraIssue> localIssueMap = BOARD_SPRINT_PARENT_MAP.get(key);

        //看板迭代不存在issue, 放入空map，防止NPE
        if (localIssueMap == null) {
            localIssueMap = new LinkedHashMap<>();
            BOARD_SPRINT_PARENT_MAP.put(key, localIssueMap);
        }

        return localIssueMap;
    }


    private static int _getIssuePagination(String boardId, String sprintId, int start, int max) {
        String key = boardId + "," + sprintId;

        String responseBody = getResponse(serverUrl + AGILE_ISSUES_URL
                .replace("{boardId}", boardId).replace("{sprintId}", sprintId)
                .replace("{start}", String.valueOf(start))
                .replace("{max}", String.valueOf(max)));

        JiraIssue.toMap(responseBody, boardId, sprintId);

        try {
            return JSON.parseObject(responseBody).getInteger("total");
        } catch (Exception e) {
            System.out.println("获取总记录数失败！！！");
            e.printStackTrace();
            return 0;
        }
    }

    public static JiraIssue getIssueCached(String issueId) {
        try {
            return ALL_ISSUE_MAP.get(issueId);
        } catch (Exception e) {
            return null;
        }
    }

    public static String getIssueById(String issueId) {
        // 通过board/sprint/查询出的issue中可能缺失某一类issue：（在ALL_ISSUE_MAP中不存在）
        // 这种issue本来应该在board/sprint下面的，但是他可能录入错了团队，
        // 导致通过 /board/xx/sprint/xx/issue/xx查不到，但是 /issue/xx 查得到
        // 所以日志只要打印了 /rest/agile/1.0/issue/16917 类似的日志，说明issue属性写错了！
        String url = serverUrl + (AGILE_ISSUE_URL.replace("{issueId}", issueId));
//        System.out.println("【警告警告警告】请求" + url + "对应的Issue属性(比如团队或不能是Epic或其他自定义属性)可能填写错误！");
        return getResponse(url);
    }

    /**
     * 先查询缓存，不存在再发起http请求查询
     * @param issueId
     * @param boardId
     * @param sprintId
     * @return
     */
    public static JiraIssue getIssueObjById(String issueId, String boardId, String sprintId) {
        JiraIssue jiraIssue = getIssueCached(issueId);
        if (jiraIssue == null) {
            System.out.println("该Issue不在缓存中,需刷新" + issueId);
            String tempIssueBody = JIRA.getIssueById(issueId);
            jiraIssue = JiraIssue._parse(JSON.parseObject(tempIssueBody), boardId, sprintId);
        }
        JIRA.ALL_ISSUE_MAP.put(jiraIssue.getIssueId(), jiraIssue);  //全局issue的集合Map
        return jiraIssue;
    }

    public static String getResponseRelative(String relativeUrl) {
        String url = serverUrl + relativeUrl;
        return getResponse(url);
    }

    public static String getResponse(String url) {
        Map<String, String> headersMap = new HashMap<String, String>();
        headersMap.put("Accept", "application/json");
        headersMap.put("X-Atlassian-Token", "nocheck");
        return getResponse(url, realm, headersMap, 5l);
    }

    public static String getResponse(String url, Realm authRealm, Map<String, String> headersMap, long timeoutSeconds) {
        System.out.println("请求数据From：" + url);

        BoundRequestBuilder requestBuilder = asyncHttpClient.prepareGet(url);

        if (authRealm !=null ) {
            requestBuilder.setRealm(authRealm);
        }
        if (headersMap != null && headersMap.size() > 0) {
            for (Map.Entry<String, String> entry : headersMap.entrySet()) {
                requestBuilder.addHeader(entry.getKey(), entry.getValue());
            }
        }
        Future<Response> f = requestBuilder.execute();
        try {
            Response r = f.get(timeoutSeconds, TimeUnit.SECONDS);
            String s = r.getResponseBody(Charset.forName("UTF-8"));
            return s;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    //显示在树节点中的名称
    public static String toIssueDisplayName(JiraIssue jiraIssue) {
        StringBuffer sb = new StringBuffer();

        sb.append("")
                .append("[").append(jiraIssue.getIssueStatus()).append("]")
                .append("[").append(jiraIssue.getIssueKey()).append("]")
                .append("[").append(jiraIssue.getIssueName()).append("]")
                .append("[").append(jiraIssue.getOwner()).append("]")
                .append("[").append(jiraIssue.getIssueId()).append("]");

        return sb.toString();
    }

    //从树节点名称中获取问题类型
    public static String getIssueStatusFromIssueDisplayName(String displayName) {
        return displayName.substring(1, displayName.indexOf("]"));
    }

    //从树节点名称中获取Id
    public static String getIdFromIssueDisplayName(String displayName) {
        int startIdx = 0;

        for (int i = displayName.length() - 1; i >= 0; i--) {
            if (displayName.charAt(i) == '[') {
                startIdx = i;
                break;
            }
        }

        return displayName.substring(startIdx + 1, displayName.length() - 1);
    }

    //根据树节点名称获取JiraIssue对象
    public static JiraIssue getObjectFromDisplayName(String displayName) {
        String id = getIdFromIssueDisplayName(displayName);
        return JIRA.getIssueCached(id);
    }


    public static void main(String[] args) throws UnsupportedEncodingException {
//        JIRA.connect("http://99.48.46.160:8080/", "nicholas.qu", "*****");
//        System.out.println(JIRA.getSprintMap("97"));
//        JIRA.disconnect();

//      Jira Rest Java API
//        final JerseyJiraRestClientFactory factory = new JerseyJiraRestClientFactory();
//        final URI jiraServerUri = new URI("http://localhost:8090/jira");
//        final JiraRestClient restClient = factory.createWithBasicHttpAuthentication(jiraServerUri, "yourusername", "yourpassword");
//        final NullProgressMonitor pm = new NullProgressMonitor();
//        final Issue issue = restClient.getIssueClient().getIssue("TST-1", pm);
//
//        System.out.println(issue);
//
//        // now let's vote for it
//        restClient.getIssueClient().vote(issue.getVotesUri(), pm);

    }
} @Data
@NoArgsConstructor
public class JiraBoard extends AbstractJiraEntity {

    private String boardId;
    private String boardName;

    //理论上一个看板只有一个project, jira api是允许多个，不知道什么考虑
    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private Map<String, JiraProject> projects = new LinkedHashMap<>();
    private boolean searched = false; //标识是否已查询过project


    public JiraBoard(String boardId) {
        this.boardId = boardId;
    }

//    {
//        "maxResults":50,
//            "startAt":0,
//            "isLast":true,
//            "values":[
//            {
//                "id":21,
//                    "self":"http://99.48.46.160:8080/rest/agile/1.0/board/21",
//                    "name":"CREDIT2 board",
//                    "type":"scrum"
//            },
//            {
//                "id":22,
//                    "self":"http://99.48.46.160:8080/rest/agile/1.0/board/22",
//                    "name":"CREDIT1 board",
//                    "type":"scrum"
//            }
//        ]
//    }

    public static Map<String, JiraBoard> toMap(String responseBody, String key) {
        Map<String, JiraBoard> boardsMap = new HashMap<String, JiraBoard>();

        JSONObject jsonObject = JSON.parseObject(responseBody);

        JSONArray boardsArray = jsonObject.getJSONArray("values");
        for (int i = 0; i < boardsArray.size(); i++) {
            JSONObject tmpJo = boardsArray.getJSONObject(i);
            JiraBoard jiraBoard = new JiraBoard();

            jiraBoard.setBoardId(String.valueOf(tmpJo.get("id")));
            jiraBoard.setBoardName(tmpJo.getString("name"));

            boardsMap.put(jiraBoard.getBoardId(), jiraBoard);
        }

        JIRA.ALL_BOARD_MAP.putAll(boardsMap);

        return boardsMap;
    }

    public Map<String, JiraProject> getProjects() {
        //2018-3-28: board查询project在需要使用的时候再查
        if (!searched) {
            projects.putAll(JIRA.getProjectMap(boardId));
            searched = true;
        }
        return projects;
    }
} @Data
@NoArgsConstructor
@AllArgsConstructor
public class JiraEpic extends AbstractJiraEntity {
    //
    private String      boardId = "";
    private String      projectId = "";
    private String      projectKey = "";
    private String      projectName = "";
    private String      epicId = "";
    private String      epicKey = "";
    private String      epicName = "";
    private String      epicSummary = "";
    private String      epicPriority = "";
    private String      epicStatus = "";
    private String      epicStatusCategoryKey = ""; //状态类型只有“待办”“进行中”“完成

    private String      epicOwner = "";

    private String[]    epicLabels;
    private String      epicDesc;
    private boolean     epicDone;

    private String      customPriority = "";
    private String      customTotalWorking = "";
    private String      customRevenueInfo = "";

    public JiraEpic(String epicId) {
        this.epicId = epicId;
    }

    //优先级的数字越大，优先级越高
    public String getCustomPriority() {
        String priorityStr = StringUtils.defaultIfBlank(getCustomField("priority"),"0");
        BigDecimal priority = new BigDecimal(priorityStr);
        return NUM_FORMAT_INT.format(priority);
    }

    public String getCustomTotalWorking() {
        return StringUtils.defaultIfBlank(getCustomField("totalworking"),"0");
    }

    public String getCost() {
        BigDecimal rate = getWorkingToCostRate();
        BigDecimal working = new BigDecimal(getCustomTotalWorking());
        return NUM_FORMAT_2D.format(rate.multiply(working));
    }

    public String getCustomRevenueInfo() {
        return getCustomField("revenueinfo");
    }

//    {
//        "expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
//            "id":"48557",
//            "self":"http://jira.immd.cn/rest/agile/1.0/issue/48557",
//            "key":"CPC-1",
//            "fields":{
//        "issuetype":{
//            "self":"http://jira.immd.cn/rest/api/2/issuetype/10000",
//                    "id":"10000",
//                    "description":"gh.issue.epic.desc",
//                    "iconUrl":"http://jira.immd.cn/images/icons/issuetypes/epic.svg",
//                    "name":"Epic",
//                    "subtask":false
//        },
//        "timespent":null,
//                "project":{
//            "self":"http://jira.immd.cn/rest/api/2/project/12800",
//                    "id":"12800",
//                    "key":"CPC",
//                    "name":"产品池",
//                    "avatarUrls":{
//                "48x48":"http://jira.immd.cn/secure/projectavatar?avatarId=10324",
//                        "24x24":"http://jira.immd.cn/secure/projectavatar?size=small&avatarId=10324",
//                        "16x16":"http://jira.immd.cn/secure/projectavatar?size=xsmall&avatarId=10324",
//                        "32x32":"http://jira.immd.cn/secure/projectavatar?size=medium&avatarId=10324"
//            }
//        },
//        "fixVersions":[
//
//        ],
//        "customfield_11001":null,
//                "customfield_11200":null,
//                "aggregatetimespent":null,
//                "resolution":null,
//                "customfield_11201":null,
//                "customfield_11400":111222,
//                "customfield_10900":null,
//                "resolutiondate":null,
//                "workratio":-1,
//                "lastViewed":"2019-07-10T17:27:48.816+0800",
//                "watches":{
//            "self":"http://jira.immd.cn/rest/api/2/issue/CPC-1/watchers",
//                    "watchCount":1,
//                    "isWatching":false
//        },
//        "created":"2019-07-09T19:39:44.000+0800",
//                "priority":{
//            "self":"http://jira.immd.cn/rest/api/2/priority/3",
//                    "iconUrl":"http://jira.immd.cn/images/icons/priorities/medium.svg",
//                    "name":"Medium",
//                    "id":"3"
//        },
//        "customfield_10100":null,
//                "labels":[
//        "nicholas",
//                "qu"
//        ],
//        "timeestimate":null,
//                "aggregatetimeoriginalestimate":null,
//                "versions":[
//
//        ],
//        "issuelinks":[
//
//        ],
//        "assignee":{
//            "self":"http://jira.immd.cn/rest/api/2/user?username=nicholas.qu",
//                    "name":"nicholas.qu",
//                    "key":"nicholas.qu",
//                    "emailAddress":"nicholas.qu@ooqi.cn",
//                    "avatarUrls":{
//                "48x48":"http://jira.immd.cn/secure/useravatar?ownerId=nicholas.qu&avatarId=11600",
//                        "24x24":"http://jira.immd.cn/secure/useravatar?size=small&ownerId=nicholas.qu&avatarId=11600",
//                        "16x16":"http://jira.immd.cn/secure/useravatar?size=xsmall&ownerId=nicholas.qu&avatarId=11600",
//                        "32x32":"http://jira.immd.cn/secure/useravatar?size=medium&ownerId=nicholas.qu&avatarId=11600"
//            },
//            "displayName":"曲健",
//                    "active":true,
//                    "timeZone":"Asia/Shanghai"
//        },
//        "updated":"2019-07-10T17:27:49.000+0800",
//                "status":{
//            "self":"http://jira.immd.cn/rest/api/2/status/10100",
//                    "description":"",
//                    "iconUrl":"http://jira.immd.cn/images/icons/status_generic.gif",
//                    "name":"进行中",
//                    "id":"10100",
//                    "statusCategory":{
//                "self":"http://jira.immd.cn/rest/api/2/statuscategory/4",
//                        "id":4,
//                        "key":"indeterminate",
//                        "colorName":"yellow",
//                        "name":"进行中"
//            }
//        },
//        "components":[
//
//        ],
//        "timeoriginalestimate":null,
//                "description":null,
//                "customfield_11100":null,
//                "customfield_11300":null,
//                "timetracking":{
//
//        },
//        "customfield_10005":"ghx-label-1",
//                "customfield_10006":null,
//                "attachment":[
//
//        ],
//        "aggregatetimeestimate":null,
//                "flagged":false,
//                "summary":"XX产品",
//                "creator":{
//            "self":"http://jira.immd.cn/rest/api/2/user?username=b.z",
//                    "name":"b.z",
//                    "key":"b.z",
//                    "emailAddress":"b.z@ooqi.cn",
//                    "avatarUrls":{
//                "48x48":"http://jira.immd.cn/secure/useravatar?ownerId=b.z&avatarId=13202",
//                        "24x24":"http://jira.immd.cn/secure/useravatar?size=small&ownerId=b.z&avatarId=13202",
//                        "16x16":"http://jira.immd.cn/secure/useravatar?size=xsmall&ownerId=b.z&avatarId=13202",
//                        "32x32":"http://jira.immd.cn/secure/useravatar?size=medium&ownerId=b.z&avatarId=13202"
//            },
//            "displayName":"张XXX",
//                    "active":true,
//                    "timeZone":"Asia/Shanghai"
//        },
//        "subtasks":[
//
//        ],
//        "reporter":{
//            "self":"http://jira.immd.cn/rest/api/2/user?username=b.z",
//                    "name":"b.z",
//                    "key":"b.z",
//                    "emailAddress":"b.z@ooqi.cn",
//                    "avatarUrls":{
//                "48x48":"http://jira.immd.cn/secure/useravatar?ownerId=b.z&avatarId=13202",
//                        "24x24":"http://jira.immd.cn/secure/useravatar?size=small&ownerId=b.z&avatarId=13202",
//                        "16x16":"http://jira.immd.cn/secure/useravatar?size=xsmall&ownerId=b.z&avatarId=13202",
//                        "32x32":"http://jira.immd.cn/secure/useravatar?size=medium&ownerId=b.z&avatarId=13202"
//            },
//            "displayName":"张XXX",
//                    "active":true,
//                    "timeZone":"Asia/Shanghai"
//        },
//        "customfield_10000":"0|i05da3:",
//                "aggregateprogress":{
//            "progress":0,
//                    "total":0
//        },
//        "customfield_10001":null,
//                "customfield_10002":null,
//                "customfield_10200":null,
//                "customfield_10003":{
//            "self":"http://jira.immd.cn/rest/api/2/customFieldOption/10000",
//                    "value":"To Do",
//                    "id":"10000"
//        },
//        "customfield_10004":"XX产品",
//                "environment":null,
//                "duedate":null,
//                "progress":{
//            "progress":0,
//                    "total":0
//        },
//        "comment":{
//            "comments":[
//
//            ],
//            "maxResults":0,
//                    "total":0,
//                    "startAt":0
//        },
//        "votes":{
//            "self":"http://jira.immd.cn/rest/api/2/issue/CPC-1/votes",
//                    "votes":0,
//                    "hasVoted":false
//        },
//        "worklog":{
//            "startAt":0,
//                    "maxResults":20,
//                    "total":0,
//                    "worklogs":[
//
//            ]
//        }
//    }
//    }

    private static final int pageSize=50;

    //产品线视图生成
    public static Map<String, Map<String, JiraIssue>> viewProductLine(String boardId) {
        Map<String, Map<String, JiraIssue>> result = new LinkedHashMap<>();

        if (StringUtils.isBlank(boardId)) {
            return result;
        }

        boolean isLast = false;
        int i = 0;

        Map<String, JiraEpic> jiraEpicMap = null;
        while (!isLast) {
            String responseBody = JIRA.getResponseRelative(JIRA.AGILE_EPICS_URL
                    .replace("{boardId}", boardId)
                    .replace("{start}", String.valueOf(i))
                    .replace("{max}", String.valueOf(pageSize)));
            isLast = JSON.parseObject(responseBody).getBooleanValue("isLast");
            i = i + pageSize;

            jiraEpicMap = _parseEpicByPage(boardId, responseBody);
        }

        Map<String, JiraIssue> jiraIssueInEpicMap = null;
        for (JiraEpic jiraEpic: jiraEpicMap.values()) {
            //将Epic下的所有issue查询出来
            jiraIssueInEpicMap = _parseIssue4Epic(jiraEpic);
            result.put(jiraEpic.getEpicId(), jiraIssueInEpicMap);
        }

        return result;
    }

    //分页解析Epic列表
    private static Map<String, JiraEpic> _parseEpicByPage(String boardId, String responseBody) {
        Map<String, JiraEpic> jiraEpicMap = JIRA.BOARD_EPICS_MAP.get(boardId);
        if (jiraEpicMap == null) {
            jiraEpicMap = new LinkedHashMap<>();
        }
        JIRA.BOARD_EPICS_MAP.put(boardId, jiraEpicMap);

        JSONObject responseJo = JSON.parseObject(responseBody);
        JSONArray  epicJsonArray = responseJo.getJSONArray("values");

        if (epicJsonArray != null && epicJsonArray.size() > 0) {

            for (int i = 0; i < epicJsonArray.size(); i++) {
                JiraEpic jiraEpic = new JiraEpic();
                JSONObject epicJo = epicJsonArray.getJSONObject(i);
                jiraEpic.setEpicId(epicJo.getString("id"));
                jiraEpic.setEpicKey(epicJo.getString("key"));
                jiraEpic.setEpicName(epicJo.getString("name"));
                jiraEpic.setEpicSummary(epicJo.getString("summary"));
                jiraEpic.setEpicDone(epicJo.getBooleanValue("done"));
                jiraEpic.setBoardId(boardId);

                //补齐Epic的详细信息
                JiraIssue issueDetail = JIRA.getIssueObjById(jiraEpic.getEpicId(), boardId, "");
                jiraEpic.setEpicOwner(issueDetail.getOwner());
                jiraEpic.setEpicPriority(issueDetail.getPriority());
                jiraEpic.setEpicStatus(issueDetail.getIssueStatus());
                jiraEpic.setStatusCategoryKey(issueDetail.getStatusCategoryKey());

                jiraEpic.setProjectId(issueDetail.getProjectId());
                jiraEpic.setProjectKey(issueDetail.getJiraProject().getProjectKey());
                jiraEpic.setProjectName(issueDetail.getJiraProject().getProjectName());

                jiraEpic.setCustomFields(issueDetail.getCustomFields());


                //刷新缓存
                JIRA.ALL_EPIC_MAP.put(jiraEpic.getEpicId(), jiraEpic);

                jiraEpicMap.put(jiraEpic.getEpicId(), jiraEpic);
            }
        }

        return jiraEpicMap;
    }

    private static Map<String, JiraIssue> _parseIssue4Epic(JiraEpic jiraEpic) {
        //每个Epic下的issue List都要重新
        Map<String, JiraIssue> jiraIssuesInEpicMap = new LinkedHashMap<>();
        if (jiraIssuesInEpicMap == null) {
            jiraIssuesInEpicMap = new LinkedHashMap<>();
        }
        JIRA.EPIC_ISSUES_MAP.put(jiraEpic.getEpicId(), jiraIssuesInEpicMap);

        StringBuffer jql = new StringBuffer();
        jql.append(" \"epic link\" = ").append(jiraEpic.getEpicKey());

        String encodedJQL = jql.toString();
        try {
            encodedJQL = URLEncoder.encode(encodedJQL, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        int pageStart = 0;
        int total = 1;
        while (total > pageStart) {
            String responseBody = JIRA.getResponseRelative(JIRA.SEARCH_BY_JQL_URL
                    .replace("{jql}", encodedJQL).replace("{start}", String.valueOf(pageStart)));

            total = JSON.parseObject(responseBody).getInteger("total");
            pageStart = pageStart + pageSize;

            Map<String, JiraIssue> jiraIssue4EpicByPageMap = JiraIssue.toMap(responseBody, "", "");

            //这里epic的信息会比jiraIssue里的完整，塞回去
            for (JiraIssue jiraIssue : jiraIssue4EpicByPageMap.values()) {
                jiraIssue.setJiraEpic(jiraEpic);
            }
            jiraIssuesInEpicMap.putAll(jiraIssue4EpicByPageMap);
        }

        return jiraIssuesInEpicMap;
    }

    public void setJSONObject(JSONObject epicJo) {
        String id = epicJo.getString("id");
        if (this.epicId != null && this.epicId.equals(id)) {
            throw new RuntimeException("已有ID "+this.epicId+ " 与 " + id + " 不一致");
        }

        this.setEpicId(id);
        this.setEpicKey(epicJo.getString("key"));
        this.setEpicName(epicJo.getString("name"));
    }


    public static void main(String[] args) {
        String s = "{\n" +
                "                    \"self\":\"http://jira.immd.cn/rest/api/2/project/12800\",\n" +
                "                    \"id\":\"12800\",\n" +
                "                    \"key\":\"CPC\",\n" +
                "                    \"name\":\"奥琪产品池\",\n" +
                "                    \"avatarUrls\":{\n" +
                "                        \"48x48\":\"http://jira.immd.cn/secure/projectavatar?avatarId=10324\",\n" +
                "                        \"24x24\":\"http://jira.immd.cn/secure/projectavatar?size=small&avatarId=10324\",\n" +
                "                        \"16x16\":\"http://jira.immd.cn/secure/projectavatar?size=xsmall&avatarId=10324\",\n" +
                "                        \"32x32\":\"http://jira.immd.cn/secure/projectavatar?size=medium&avatarId=10324\"\n" +
                "                    }\n" +
                "                }";

        System.out.println(JSON.parseObject(s).getDouble("id"));
    }
} @Data
@NoArgsConstructor
@AllArgsConstructor
public class JiraIssue extends AbstractJiraEntity {
    private String issueType = "";
    private String issueId = "";
    private String issueKey = "";
    private String issueName = "";
    private String issueStatus = "";

    private String owner = "";

    private String estimate = "";   //估算时间
    private String remainingTime = ""; //剩余时间
    private String estimateInSeconds = "";   //估算时间(单位秒)
    private String remainingTimeInSeconds = ""; //剩余时间(单位秒)

    private String parentType = "";
    private String parentId = "";
    private String parentKey = "";
    private String parentName = "";

    private JiraProject jiraProject;
//    private String boardId = "";        //从jListBoard中获取，issue的接口中的orginalBoardId是错误的（ETS而不是ETS-Arch的看板）
    private JiraBoard  jiraBoard;
    private JiraSprint jiraSprint;
    private JiraEpic   jiraEpic;

    //add at 2017.9.25
    private String priority = "";
    private String createdAt = "";
    private String updatedAt = "";
    private String description = "";
    private String dueDate = "";
    private String comments = "";

    private String allJson; //缓存所有的json数据

    private boolean hasSubtask = false;
    private Map<String, JiraIssue> subTaskMap;
    private JSONArray subTaskJo;

    public JiraIssue(JiraEpic jiraEpic) {
        this.jiraEpic = jiraEpic;
    }

    private String formatDate(String d) {
        try {
            if (StringUtils.isNotBlank(d)) {
                return defaultDateFormat.format(jiraDateFormat.parse(d));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return d;
    }

    public String getCreatedAt() {
        return formatDate(this.createdAt);
    }

    public String getUpdatedAt() {
        return formatDate(this.updatedAt);
    }

    public int getPriorityNum() {
        if (priority == null) return 99;

        String priorityToNumJson = PropertiesCache.getProp(PropertiesCache.P_ISSUE_PRIORITY_KEY_TO_NUM);
        Map<String, Integer> priorityToNumMap = null;
        try {
            priorityToNumMap = JSON.parseObject(priorityToNumJson, Map.class);
        } catch (Exception ignore) {
            priorityToNumMap.put("highest", 10);
            priorityToNumMap.put("high", 30);
            priorityToNumMap.put("medium", 50);
            priorityToNumMap.put("low", 70);
            priorityToNumMap.put("lowest", 90);
        }

        return priorityToNumMap.get(priority) == null ? 50 : priorityToNumMap.get(priority);
    }

    //倒排序 填充固定长度的优先级数字,JIRA内的priority输入的数值最多只允许3位
    public String getPriority4Sort() {
        return String.valueOf(getPriorityNum());
    }

    public String getCustomEstimateDone() {
        String estimateDone = getCustomField("estimatedone");
        if (StringUtils.isNotBlank(estimateDone)) {
            return formatDate(estimateDone);
        } else {
            return "";
        }
    }
    //优先级的数字越大，优先级越高
    public String getCustomActualDone() {
        String actualdone = getCustomField("actualdone");
        if (StringUtils.isNotBlank(actualdone)) {
            return formatDate(actualdone);
        } else {
            return "";
        }
    }

//    {
//        "expand":"schema,names",
//            "startAt":0,
//            "maxResults":50,
//            "total":52,
//            "issues":[
//                {
//                    "expand":"operations,versionedRepresentations,editmeta,changelog,renderedFields",
//                        "id":"15650",
//                        "self":"http://99.48.46.160:8080/rest/agile/1.0/issue/15650",
//                        "key":"ETS-119",
//                        "fields":{
//                    "issuetype":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10001",
//                                "id":"10001",
//                                "description":"gh.issue.story.desc",
//                                "iconUrl":"http://99.48.46.160:8080/images/icons/issuetypes/story.svg",
//                                "name":"Story",
//                                "subtask":false
//                    },
//                    "timespent":null,
//                            "sprint":{
//                        "id":88,
//                                "self":"http://99.48.46.160:8080/rest/agile/1.0/sprint/88",
//                                "state":"active",
//                                "name":"ETS 0915 Sprint",
//                                "startDate":"2017-09-04T11:55:16.390+08:00",
//                                "endDate":"2017-09-15T11:55:00.000+08:00",
//                                "originBoardId":97
//                    },
//                    "project":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/project/11100",
//                                "id":"11100",
//                                "key":"ETS",
//                                "name":"ETS",
//                                "avatarUrls":{
//                            "48x48":"http://99.48.46.160:8080/secure/projectavatar?avatarId=10324",
//                                    "24x24":"http://99.48.46.160:8080/secure/projectavatar?size=small&avatarId=10324",
//                                    "16x16":"http://99.48.46.160:8080/secure/projectavatar?size=xsmall&avatarId=10324",
//                                    "32x32":"http://99.48.46.160:8080/secure/projectavatar?size=medium&avatarId=10324"
//                        }
//                    },
//                    "fixVersions":[
//
//                        ],
//                    "aggregatetimespent":null,
//                            "resolution":null,
//                            "customfield_10104":null,
//                            "customfield_10105":null,
//                            "customfield_10106":null,
//                            "customfield_10107":null,
//                            "resolutiondate":null,
//                            "workratio":-1,
//                            "lastViewed":null,
//                            "watches":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/issue/ETS-119/watchers",
//                                "watchCount":1,
//                                "isWatching":false
//                    },
//                    "created":"2017-08-07T18:40:53.000+0800",
//                            "epic":{
//                        "id":15645,
//                                "key":"ETS-114",
//                                "self":"http://99.48.46.160:8080/rest/agile/1.0/epic/15645",
//                                "name":"消息中心",
//                                "summary":"消息中心",
//                                "color":{
//                            "key":"color_3"
//                        },
//                        "done":false
//                    },
//                    "priority":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                "name":"Medium",
//                                "id":"3"
//                    },
//                    "customfield_10100":null,
//                            "customfield_10101":null,
//                            "customfield_10102":null,
//                            "labels":[
//
//                        ],
//                    "customfield_10103":null,
//                            "timeestimate":null,
//                            "aggregatetimeoriginalestimate":201630,
//                            "versions":[
//
//                        ],
//                    "issuelinks":[
//
//                        ],
//                    "assignee":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/user?username=qiaojicheng",
//                                "name":"qiaojicheng",
//                                "key":"qiaojicheng",
//                                "emailAddress":"jicheng.qiao@mi-me.com",
//                                "avatarUrls":{
//                            "48x48":"http://99.48.46.160:8080/secure/useravatar?ownerId=qiaojicheng&avatarId=11601",
//                                    "24x24":
//                            "http://99.48.46.160:8080/secure/useravatar?size=small&ownerId=qiaojicheng&avatarId=11601",
//                                    "16x16":
//                            "http://99.48.46.160:8080/secure/useravatar?size=xsmall&ownerId=qiaojicheng&avatarId=11601",
//                                    "32x32":
//                            "http://99.48.46.160:8080/secure/useravatar?size=medium&ownerId=qiaojicheng&avatarId=11601"
//                        },
//                        "displayName":"乔继成",
//                                "active":true,
//                                "timeZone":"Asia/Shanghai"
//                    },
//                    "updated":"2017-09-04T15:19:32.000+0800",
//                            "status":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/status/10408",
//                                "description":"",
//                                "iconUrl":"http://99.48.46.160:8080/",
//                                "name":"待办",
//                                "id":"10408",
//                                "statusCategory":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/2",
//                                    "id":2,
//                                    "key":"new",
//                                    "colorName":"blue-gray",
//                                    "name":"待办"
//                        }
//                    },
//                    "components":[
//
//                        ],
//                    "timeoriginalestimate":null,
//                            "description":"作为海米业务系统，我想要和消息中心进行解耦，这样我就可以使用消息中心的模板进行独立配置消息，并通过消息中心发送消息，而不用消息中心做任何配置。",
//                            "timetracking":{
//
//                    },
//                    "customfield_10006":null,
//                            "customfield_10601":[
//                    {
//                        "self":"http://99.48.46.160:8080/rest/api/2/customFieldOption/10708",
//                            "value":"架构",
//                            "id":"10708"
//                    }
//                        ],
//                    "attachment":[
//
//                        ],
//                    "customfield_10603":null,
//                            "aggregatetimeestimate":172800,
//                            "flagged":false,
//                            "summary":"消息网关介入海米（原消息平台改造）",
//                            "creator":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/user?username=yan.qian",
//                                "name":"yan.qian",
//                                "key":"yan.qian",
//                                "emailAddress":"yan.qian@mi-me.com",
//                                "avatarUrls":{
//                            "48x48":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=48",
//                                    "24x24":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=24",
//                                    "16x16":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=16",
//                                    "32x32":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=32"
//                        },
//                        "displayName":"钱雁",
//                                "active":true,
//                                "timeZone":"Asia/Shanghai"
//                    },
//                    "subtasks":[
//                    {
//                        "id":"16257",
//                            "key":"ETS-348",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16257",
//                            "fields":{
//                        "summary":"生产端组件业务系统接入启动会",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16258",
//                            "key":"ETS-349",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16258",
//                            "fields":{
//                        "summary":"现金贷进度跟进",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16271",
//                            "key":"ETS-358",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16271",
//                            "fields":{
//                        "summary":"技术支持联调",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16309",
//                            "key":"ETS-370",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16309",
//                            "fields":{
//                        "summary":"接入qq沟通群创建",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16311",
//                            "key":"ETS-372",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16311",
//                            "fields":{
//                        "summary":"接入资料wiki整理",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16392",
//                            "key":"ETS-416",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16392",
//                            "fields":{
//                        "summary":"消息中心-生产端新增业务消息发送功能方案设计并和业务方沟通",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16407",
//                            "key":"ETS-419",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16407",
//                            "fields":{
//                        "summary":"消息中心-生产端新增业务方消息发送功能编码开发并自测",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16609",
//                            "key":"ETS-438",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16609",
//                            "fields":{
//                        "summary":"目前现金贷业务为海米APP提供新消息发送进度跟踪",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10100",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/status_generic.gif",
//                                    "name":"进行中",
//                                    "id":"10100",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/4",
//                                        "id":4,
//                                        "key":"indeterminate",
//                                        "colorName":"yellow",
//                                        "name":"进行中"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16916",
//                            "key":"ETS-569",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16916",
//                            "fields":{
//                        "summary":"新增爱海米APP消息模板",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    },
//                    {
//                        "id":"16918",
//                            "key":"ETS-571",
//                            "self":"http://99.48.46.160:8080/rest/api/2/issue/16918",
//                            "fields":{
//                        "summary":"修改代码支持爱海米消息发送",
//                                "status":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/status/10001",
//                                    "description":"",
//                                    "iconUrl":"http://99.48.46.160:8080/",
//                                    "name":"完成",
//                                    "id":"10001",
//                                    "statusCategory":{
//                                "self":"http://99.48.46.160:8080/rest/api/2/statuscategory/3",
//                                        "id":3,
//                                        "key":"done",
//                                        "colorName":"green",
//                                        "name":"Done"
//                            }
//                        },
//                        "priority":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/priority/3",
//                                    "iconUrl":"http://99.48.46.160:8080/images/icons/priorities/medium.svg",
//                                    "name":"Medium",
//                                    "id":"3"
//                        },
//                        "issuetype":{
//                            "self":"http://99.48.46.160:8080/rest/api/2/issuetype/10006",
//                                    "id":"10006",
//                                    "description":"问题的子任务",
//                                    "iconUrl":
//                            "http://99.48.46.160:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
//                                    "name":"子任务",
//                                    "subtask":true,
//                                    "avatarId":10316
//                        }
//                    }
//                    }
//                        ],
//                    "reporter":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/user?username=yan.qian",
//                                "name":"yan.qian",
//                                "key":"yan.qian",
//                                "emailAddress":"yan.qian@mi-me.com",
//                                "avatarUrls":{
//                            "48x48":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=48",
//                                    "24x24":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=24",
//                                    "16x16":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=16",
//                                    "32x32":"http://www.gravatar.com/avatar/2ad0f4aa3da8bfa5fae5195cbc4f5fed?d=mm&s=32"
//                        },
//                        "displayName":"钱雁",
//                                "active":true,
//                                "timeZone":"Asia/Shanghai"
//                    },
//                    "customfield_10000":"0|i00r02:zzzx",
//                            "aggregateprogress":{
//                        "progress":0,
//                                "total":172800,
//                                "percent":0
//                    },
//                    "customfield_10001":[
//                    "com.atlassian.greenhopper.service.sprint.Sprint@638148bd[id=84,rapidViewId=95,state=CLOSED,name=ETS 0901 Sprint,startDate=2017-08-21T12:58:21.482+08:00,endDate=2017-09-01T12:58:00.000+08:00,completeDate=2017-09-04T11:51:53.968+08:00,sequence=84]",
//                            "com.atlassian.greenhopper.service.sprint.Sprint@72c45c71[id=88,rapidViewId=97,state=ACTIVE,name=ETS 0915 Sprint,startDate=2017-09-04T11:55:16.390+08:00,endDate=2017-09-15T11:55:00.000+08:00,completeDate=<null>,sequence=88]"
//                        ],
//                    "customfield_10002":"ETS-114",
//                            "customfield_10200":null,
//                            "environment":null,
//                            "duedate":null,
//                            "closedSprints":[
//                    {
//                        "id":84,
//                            "self":"http://99.48.46.160:8080/rest/agile/1.0/sprint/84",
//                            "state":"closed",
//                            "name":"ETS 0901 Sprint",
//                            "startDate":"2017-08-21T12:58:21.482+08:00",
//                            "endDate":"2017-09-01T12:58:00.000+08:00",
//                            "completeDate":"2017-09-04T11:51:53.968+08:00",
//                            "originBoardId":95
//                    }
//                        ],
//                    "progress":{
//                        "progress":0,
//                                "total":0
//                    },
//                    "comment":{
//                        "comments":[
//
//                            ],
//                        "maxResults":0,
//                                "total":0,
//                                "startAt":0
//                    },
//                    "votes":{
//                        "self":"http://99.48.46.160:8080/rest/api/2/issue/ETS-119/votes",
//                                "votes":0,
//                                "hasVoted":false
//                    },
//                    "worklog":{
//                        "startAt":0,
//                                "maxResults":20,
//                                "total":0,
//                                "worklogs":[
//
//                            ]
//                    }
//                }
//                }
//        ]
//    }

    //解析应答，顺序与Jira看板保持一致
    public static Map<String, JiraIssue> toMapWithKanbanOrder(String responseBody, String boardId, String sprintId) {
        Map<String, JiraIssue> sameOrderIssueMap = new LinkedHashMap<String, JiraIssue>();
        Map<String, JiraIssue> noSubTaskIssueMap = new LinkedHashMap<String, JiraIssue>(); //没有子任务的issue沉到底端 在看板中是Other Issue一类

        JSONObject responseJo = JSON.parseObject(responseBody);
        JSONArray  issuesArrayInJiraOrder = responseJo.getJSONObject("issuesData").getJSONArray("issues");

        if (issuesArrayInJiraOrder != null && issuesArrayInJiraOrder.size() > 0) {

            for (int i = 0; i < issuesArrayInJiraOrder.size(); i++) {
                String issueId = String.valueOf(issuesArrayInJiraOrder.getJSONObject(i).getInteger("id"));

                JiraIssue issueObj = JIRA.getIssueObjById(issueId, boardId, sprintId);

                _handleMap(issueObj, boardId, sprintId, sameOrderIssueMap, noSubTaskIssueMap);
            }
        }

        sameOrderIssueMap.putAll(noSubTaskIssueMap);

        return sameOrderIssueMap;
    }

    private static void _handleMap(JiraIssue issueObj,
                                  String boardId, String sprintId,
                                  Map<String, JiraIssue> sameOrderIssueMap,
                                  Map<String, JiraIssue> noSubTaskIssueMap) {
        JiraIssue parentIssue = null;

        boolean needPutParent = false;
        String parentId = issueObj.getParentId();
        if (issueObj.isParent()) {
            parentIssue = issueObj;
            needPutParent = true;
        } else if (parentIssue == null
                || (parentId != null && parentId.length()> 0
                && !parentIssue.getIssueId().equals(parentId))) {
            //由于Jira过滤器的存在，可能parent的团队录错了，导致api返回的第一个值是子任务，在报文中会出现 missingParents 的字段
            parentIssue = JIRA.getIssueObjById(parentId, boardId, sprintId);
            needPutParent = true;
        }

        if (needPutParent && parentIssue != null) {
            if (parentIssue.hasSubtask) {
                //树形结构里只放epic, story或者任务，等可包含子任务的节点
                sameOrderIssueMap.put(parentIssue.getIssueId(), parentIssue);
            } else {
                noSubTaskIssueMap.put(parentIssue.getIssueId(), parentIssue);
            }
        }

        if (!issueObj.isParent()) {
            Map<String, JiraIssue> subtaskMap = parentIssue.getSubTaskMap();
            if (subtaskMap == null) {
                subtaskMap = new LinkedHashMap<String, JiraIssue>();
            }

            subtaskMap.put(issueObj.getIssueId(), issueObj);
            parentIssue.setSubTaskMap(subtaskMap);
        }
    }

    //优先级（high medium low）, 产品状态（doing>todo>done）
    public Map<String, JiraIssue> getSortedSubTaskMap() {
        Map<String, JiraIssue> sortedSubTaskMap = new TreeMap<>();

        for (JiraIssue subJiraIssue : this.getSubTaskMap().values()) {
            sortedSubTaskMap.put(subJiraIssue.getSortedKey(), subJiraIssue);
        }

        return sortedSubTaskMap;
    }

    public String getSortedKey() {
        return this.getPriority4Sort()
                + this.getStatusCategory4Sort()
                + StringUtils.leftPad(this.getIssueId(), 10, "0");
    }


    //已关闭的Sprint从缓存里面取issue
    public static Map<String, JiraIssue> toMap4FromCache(Map<String, JiraIssue> jiraIssueMap,String boardId, String sprintId) {
        Map<String, JiraIssue> sameOrderIssueMap = new LinkedHashMap<String, JiraIssue>();
        Map<String, JiraIssue> noSubTaskIssueMap = new LinkedHashMap<String, JiraIssue>(); //没有子任务的issue沉到底端 在看板中是Other Issue一类

        if (jiraIssueMap != null && jiraIssueMap.size() > 0) {
            for (JiraIssue issueObj : jiraIssueMap.values()) {
                if (issueObj.hasSubtask) {
                    JSONArray subIssueArray = issueObj.getSubTaskJo();
                    sameOrderIssueMap.put(issueObj.getIssueId(), issueObj);

                    for (int i = 0; i < subIssueArray.size(); i++) {
                        String subissueId = String.valueOf(subIssueArray.getJSONObject(i).getInteger("id"));
                        JiraIssue subIssue = JIRA.getIssueObjById(subissueId, boardId, sprintId);

                        Map<String, JiraIssue> subIssueMap = issueObj.getSubTaskMap();
                        if (subIssueMap == null) subIssueMap = new LinkedHashMap<String, JiraIssue>();
                        subIssueMap.put(subissueId, subIssue);
                        issueObj.setSubTaskMap(subIssueMap);
                    }
                }
            }
        }

        return sameOrderIssueMap;
    }

    //解析缓存用
    public static Map<String, JiraIssue> toMap(String responseBody, String boardId, String sprintId) {
        Map<String, JiraIssue> localIssueMap = new LinkedHashMap<>();

        JSONObject jsonObject = JSON.parseObject(responseBody);

        JSONArray issueArray = jsonObject.getJSONArray("issues");
        if (issueArray == null)  return localIssueMap;

        for (int i = 0; i < issueArray.size(); i++) {
            JSONObject backlogJo = issueArray.getJSONObject(i);
            JSONObject fieldJo = backlogJo.getJSONObject("fields");

            JiraIssue backlog = _parse(backlogJo, boardId, sprintId);

            localIssueMap.put(backlog.getIssueId(), backlog);            //本次issue的集合Map
            JIRA.ALL_ISSUE_MAP.put(backlog.getIssueId(), backlog);  //全局issue的集合Map

            JSONArray subTasksJo = fieldJo.getJSONArray("subtasks");
            backlog.setSubTaskJo(subTasksJo);
        }

        //树形结构里只放story或者任务，用于展示
        for (JiraIssue jiraIssue : localIssueMap.values()) {
            if (jiraIssue.isParent()) {
                JIRA.putStoryOrTaskMap(boardId, sprintId, jiraIssue.getIssueId(), jiraIssue);
            }
        }

        return localIssueMap;
    }

    public boolean isParent() {
        return !isSubTask();
    }
//    public boolean isStoryOrTask() {
//        return isStory() || isTask();
//    }
    public boolean isEpic() {
        return "Epic".equalsIgnoreCase(this.getIssueType());
    }
    public boolean isStory() {
        return "Story".equalsIgnoreCase(this.getIssueType());
    }
    public boolean isTask() {
        return "任务".equalsIgnoreCase(this.getIssueType());
    }
    public boolean isSubTask() {
        return "子任务".equalsIgnoreCase(this.getIssueType());
    }

    public void setProjectId(String projectId) {
        getJiraProject().setProjectId(projectId);
    }

    public String getProjectId() {
        return this.getJiraProject().getProjectId();
    }

    public JiraProject getJiraProject() {
        if (this.jiraProject == null) {
            this.jiraProject = new JiraProject();
        }
        return this.jiraProject;
    }

    public void setBoardId(String boardId) {
        getJiraBoard().setBoardId(boardId);
    }
    public String getBoardId() {
        return this.getJiraBoard().getBoardId();
    }

    public JiraBoard getJiraBoard() {
        if (this.jiraBoard == null) {
            this.jiraBoard = new JiraBoard();
        }
        return this.jiraBoard;
    }

    public void setSprintId(String sprintId) {
        getJiraSprint().setSprintId(sprintId);
    }

    public String getSprintId() {
        return this.getJiraSprint().getSprintId();
    }

    public JiraSprint getJiraSprint() {
        if (this.jiraSprint == null) {
            this.jiraSprint = new JiraSprint();
        }
        return this.jiraSprint;
    }

    public void setEpicId(String epicId) {
        getJiraEpic().setEpicId(epicId);
    }

    public JiraEpic getJiraEpic() {
        if (this.jiraEpic == null) {
            this.jiraEpic = new JiraEpic();
        }
        return this.jiraEpic;
    }

    public static JiraIssue _parse(JSONObject inputJo, String boardId, String sprintId) {
        refreshStatusCategory();

        JiraIssue jiraIssue = new JiraIssue();
        jiraIssue.setAllJson(inputJo.toJSONString());

        jiraIssue.setIssueId(inputJo.getString("id"));
        jiraIssue.setIssueKey(inputJo.getString("key"));
        jiraIssue.setBoardId(boardId);
        jiraIssue.setSprintId(sprintId);

        JSONObject fieldJo = inputJo.getJSONObject("fields");
        if (fieldJo == null) return jiraIssue;

        jiraIssue.setIssueType(fieldJo.getJSONObject("issuetype").getString("name"));
        jiraIssue.setIssueName(fieldJo.getString("summary"));
        jiraIssue.setIssueStatus(fieldJo.getJSONObject("status").getString("name"));
        jiraIssue.setStatusCategoryKey(fieldJo.getJSONObject("status").getJSONObject("statusCategory").getString("key"));

        //设置Sprint对象
        JSONObject sprintJo = fieldJo.getJSONObject("sprint");
        if (sprintJo == null) {
            //只有spring active的时候才能取到sprint属性，否则是closeSprints数组
            JSONArray sprintArray = fieldJo.getJSONArray("closedSprints");
            if (sprintArray != null) {
                sprintJo = sprintArray.getJSONObject(0);

                if (sprintId != null) {
                    for (int i = 0; i < sprintArray.size(); i++) {
                        sprintJo = sprintArray.getJSONObject(i);
                        if (sprintId.equals(sprintJo.getString("id"))) {
                            break;
                        }
                    }
                }
            }
        }
        if (sprintJo != null) {
            jiraIssue.getJiraSprint().setJSONObject(sprintJo);
        }

        //设置Project对象
        JSONObject projectJo = fieldJo.getJSONObject("project");
        if (projectJo != null) {
            jiraIssue.getJiraProject().setJSONObject(projectJo);
        }

        //设置Epic对象
        JSONObject epicJo = fieldJo.getJSONObject("epic");
        if (epicJo != null) {
            jiraIssue.getJiraEpic().setJSONObject(epicJo);
        }

        try {
            jiraIssue.setOwner(nullToBlank(fieldJo.getJSONObject("assignee").getString("displayName")));
        } catch (Exception e) {
            jiraIssue.setOwner("");
        }

        try {
            jiraIssue.setEpicId(nullToBlank(fieldJo.getJSONObject("epic").getString("id")));
        } catch (Exception e) {
            jiraIssue.setEpicId("");
        }

        try {
            jiraIssue.getJiraEpic().setEpicKey(nullToBlank(fieldJo.getJSONObject("epic").getString("key")));
        } catch (Exception e) {
            jiraIssue.getJiraEpic().setEpicKey("");
        }

        try {
            jiraIssue.getJiraEpic().setEpicName(nullToBlank(fieldJo.getJSONObject("epic").getString("summary")));
        } catch (Exception e) {
            jiraIssue.getJiraEpic().setEpicName("");
        }

        try {
            jiraIssue.setEstimate(nullToBlank(fieldJo.getJSONObject("timetracking").getString("originalEstimate")));
            jiraIssue.setEstimateInSeconds(nullToBlank(fieldJo.getJSONObject("timetracking").getString("originalEstimateSeconds")));
        } catch (Exception e) {
            jiraIssue.setEstimate("");
            jiraIssue.setEstimateInSeconds("");
        }
        try {
            jiraIssue.setRemainingTime(nullToBlank(fieldJo.getJSONObject("timetracking").getString("remainingEstimate")));
            jiraIssue.setRemainingTimeInSeconds(nullToBlank(fieldJo.getJSONObject("timetracking").getString("remainingEstimateSeconds")));
        } catch (Exception e) {
            jiraIssue.setRemainingTime("");
            jiraIssue.setRemainingTimeInSeconds("");
        }

        try {
            jiraIssue.setPriority(nullToBlank(fieldJo.getJSONObject("priority").getString("name")));
        } catch (Exception e) {
            jiraIssue.setPriority("");
        }
        try {
            jiraIssue.setCreatedAt(nullToBlank(fieldJo.getString("created")));
        } catch (Exception e) {
            jiraIssue.setCreatedAt("");
        }
        try {
            jiraIssue.setUpdatedAt(nullToBlank(fieldJo.getString("updated")));
        } catch (Exception e) {
            jiraIssue.setUpdatedAt("");
        }
        try {
            jiraIssue.setDescription(nullToBlank(fieldJo.getString("description")));
        } catch (Exception e) {
            jiraIssue.setDescription("");
        }
        try {
            jiraIssue.setDueDate(nullToBlank(fieldJo.getString("duedate")));
        } catch (Exception e) {
            jiraIssue.setDueDate("");
        }

        StringBuffer commentStr = new StringBuffer();
        try {
            JSONObject commentArrayJO = fieldJo.getJSONObject("comment");
            int totalComments = commentArrayJO.getInteger("total");

            if (totalComments > 0) {
                JSONArray commentsArray = commentArrayJO.getJSONArray("comments");
                //倒排序只取最后的3个（JIRA默认是按照created时间正排序的，这里因为只取3个，不用updated倒排序了）
                for (int i = commentsArray.size() - 1; i >= 0 && i >= commentsArray.size() - 3; i--) {
                    JSONObject commentJO = commentsArray.getJSONObject(i);
                    String updated = commentJO.getString("updated");
                    String body = commentJO.getString("body");
                    String author = commentJO.getJSONObject("author").getString("displayName");

                    String updatedFormtted = jiraIssue.defaultDateFormat.format(jiraIssue.jiraDateFormat.parse(updated));

                    if (commentStr.length() > 0) {
                        commentStr.append("\r\n");
                    }
                    commentStr.append(updatedFormtted).append(" ").append(author).append(": \r\n").append(body);
                }
            }
        } catch (Exception e) {
        }
        jiraIssue.setComments(commentStr.toString());


        //2018-3-22 Nicholas
        //递归解析JSON映射关系Map, 调用JIRA.AGILE_ISSUE_URL返回的JSON。
        //根节点是基于fields，对应的动态添加的Key写法应该是 fields>assignee>name
        //示例：
//        fields>assignee>displayName ============= 曲
//        fields>assignee>name ============= nicholas.qu
//        fields>assignee>self ============= http://xxxxx/rest/api/2/user?username=debao.fan
//        fields>assignee>active ============= true
//        fields>assignee>timeZone ============= Asia/Shanghai
//        fields>assignee>key ============= nicholas
//        fields>worklog>total ============= 0
//        fields>worklog>maxResults ============= 20
//        fields>worklog>startAt ============= 0
//        fields>updated ============= 2018-03-16T17:34:54.000+0800
        jiraIssue.getCustomFields().putAll(JSONParsingUtil.getAllMapping(inputJo.toString()));

        //Team customfield_10601
        try {
            jiraIssue.getCustomFields().put("teamName", nullToBlank(fieldJo.getJSONArray("customfield_10601").getJSONObject(0).getString("value")));
            jiraIssue.getCustomFields().put("teamId",   nullToBlank(fieldJo.getJSONArray("customfield_10601").getJSONObject(0).getString("id")));
        } catch (Exception e) {
            jiraIssue.getCustomFields().put("teamName", "");
            jiraIssue.getCustomFields().put("teamId",   boardId);
        }

        JSONObject parentJo = fieldJo.getJSONObject("parent");
        if (parentJo != null && parentJo.size() > 0) {
            jiraIssue.setParentId(nullToBlank(parentJo.getString("id")));
            jiraIssue.setParentKey(nullToBlank(parentJo.getString("key")));
            jiraIssue.setParentName(nullToBlank(parentJo.getJSONObject("fields").getString("summary")));
            jiraIssue.setParentType(nullToBlank(parentJo.getJSONObject("fields").getJSONObject("issuetype").getString("name")));

        } else if (jiraIssue.isParent()) { //(jiraIssue.getEpicKey() != null && jiraIssue.getEpicKey().length() > 0) {
            jiraIssue.setParentType("Epic");
            jiraIssue.setParentId(nullToBlank(jiraIssue.getJiraEpic().getEpicId()));
            jiraIssue.setParentKey(nullToBlank(jiraIssue.getJiraEpic().getEpicKey()));
            jiraIssue.setParentName(nullToBlank(jiraIssue.getJiraEpic().getEpicName()));
        }

        JSONArray subTasksJo = fieldJo.getJSONArray("subtasks");
        if (subTasksJo == null || subTasksJo.size() == 0) {
            jiraIssue.setHasSubtask(false);
        } else {
            jiraIssue.setHasSubtask(true);
        }

        return jiraIssue;
    }


} @Data
@NoArgsConstructor
@AllArgsConstructor
public class JiraSprint extends AbstractJiraEntity {

    private String sprintId;
    private String sprintName;
    private String startDate;
    private String endDate;
    private String completeDate;
    private String state; //closed, active

    public JiraSprint(String sprintId) {
        this.sprintId = sprintId;
    }

    //    {
//        "maxResults":50,
//            "startAt":0,
//            "isLast":true,
//            "values":[
//            {
//                "id":83,
//                    "self":"http://99.48.46.160:8080/rest/agile/1.0/sprint/83",
//                    "state":"closed",
//                    "name":"ETS 0818 Sprint",
//                    "startDate":"2017-08-07T09:18:04.527+08:00",
//                    "endDate":"2017-08-18T09:18:00.000+08:00",
//                    "completeDate":"2017-08-21T12:58:45.934+08:00",
//                    "originBoardId":95
//            },
//            {
//                "id":88,
//                    "self":"http://99.48.46.160:8080/rest/agile/1.0/sprint/88",
//                    "state":"active",
//                    "name":"ETS 0915 Sprint",
//                    "startDate":"2017-09-04T11:55:16.390+08:00",
//                    "endDate":"2017-09-15T11:55:00.000+08:00",
//                    "originBoardId":97
//            }
//        ]
//    }


    /**
     * Key = SprintName
     * Value = JiraSprint
     * @param responseBody
     * @return
     */
    public static Map<String, JiraSprint> toMap(String responseBody, String key) {
        Map<String, JiraSprint> localSprintsMap = new TreeMap<String, JiraSprint>();
        Map<String, JiraSprint> historySprintsMap = new TreeMap<String, JiraSprint>();

        JSONObject jsonObject = JSON.parseObject(responseBody);

        JSONArray sprintArray = jsonObject.getJSONArray("values");

        if (sprintArray != null) {
            for (int i = 0; i < sprintArray.size(); i++) {
                JSONObject tmpJo = sprintArray.getJSONObject(i);
                JiraSprint jiraSprint = new JiraSprint();
                jiraSprint.setSprintId(String.valueOf(tmpJo.get("id")));
                jiraSprint.setSprintName(tmpJo.getString("name"));
                jiraSprint.setStartDate(tmpJo.getString("startDate"));
                jiraSprint.setEndDate(tmpJo.getString("endDate"));
                jiraSprint.setState(tmpJo.getString("state"));

                if (jiraSprint.isActive()) {
                    localSprintsMap.put(jiraSprint.getSprintId(), jiraSprint);
                } else {
                    historySprintsMap.put(jiraSprint.getSprintId(), jiraSprint);
                }
            }
        }

        Map<String, JiraSprint> thisTimeSprintMap = JIRA.BOARD_SPRINT_MAP.get(key);
        if (thisTimeSprintMap == null) {
            thisTimeSprintMap = new LinkedHashMap<>();
        }
        thisTimeSprintMap.putAll(localSprintsMap);
        thisTimeSprintMap.putAll(historySprintsMap);

        JIRA.BOARD_SPRINT_MAP.put(key, thisTimeSprintMap);

        JIRA.ALL_SPRINT_MAP.putAll(localSprintsMap);
        JIRA.ALL_SPRINT_MAP.putAll(historySprintsMap);

        return localSprintsMap;
    }

    public boolean isActive() {
        return "active".equalsIgnoreCase(this.getState());
    }

    public void setJSONObject(JSONObject sprintJo) {
        String id = sprintJo.getString("id");
        if (StringUtils.equals(this.sprintId, id)) {
            throw new RuntimeException("已有ID "+this.sprintId+ " 与 " + id + " 不一致");
        }

        this.setSprintId(id);
        this.setSprintName(sprintJo.getString("name"));
        this.setState(sprintJo.getString("state"));
        this.setStartDate(sprintJo.getString("startDate"));
        this.setEndDate(sprintJo.getString("endDate"));
        this.setCompleteDate(sprintJo.getString("completeDate"));
    }

} public class JiraAutoUpdater {
    private static final String URL_LATEST_TAG = "https://api.github.com/repos/NicholasQu/JiraScrumCardsPrinter/releases/latest";
    private static final String URL_JAR_DOWNLOAD = "https://github.com/NicholasQu/JiraScrumCardsPrinter/releases/download/{tagName}/JiraScrumCardsPrinter-1.0.jar-jar-with-dependencies.jar";

    private static AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient();

    public static String LATEST_TAG_NAME = "0";
    public static String LATEST_DOWN_URL = "http://github.xxx/";
    public static String LATEST_DESC = "release notes";
    public static double LATEST_ASSET_SIZE = 9633289.0;
    public static File LATEST_TAG_IDENTIFIER = null;

    public static AtomicInteger GIT_NOT_CONNECTED_COUNT = new AtomicInteger(0);

    //True: have updates
    public static boolean checkUpdates() {
        Future<Response> f = asyncHttpClient.prepareGet(URL_LATEST_TAG)
                .addHeader("Accept", "application/json")
                .execute();
        String responseBody;
        try {
            Response r = f.get(5L, TimeUnit.SECONDS);
            responseBody = r.getResponseBody(Charset.forName("UTF-8"));
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        InputStream in = JiraAutoUpdater.class.getResourceAsStream("/jscp_version");
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        String currentJarVersion = null;
        try {
            currentJarVersion = reader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        JSONObject jo = JSON.parseObject(responseBody);

        //检查最新的tag是否已经下载过，即查看 TAG_XXX 这个文件是否存在
        LATEST_TAG_NAME = jo.getString("tag_name");
        LATEST_DESC = jo.getString("body");

        try {
            JSONObject asset = jo.getJSONArray("assets").getJSONObject(0);

            LATEST_ASSET_SIZE = asset.getDoubleValue("size");

            LATEST_DOWN_URL = PropertiesCache.getUpdateUrl();
            if (StringUtils.isBlank(LATEST_DOWN_URL)) {
                LATEST_DOWN_URL = asset.getString("browser_download_url");
                if (LATEST_DOWN_URL == null) {
                    LATEST_DOWN_URL = URL_JAR_DOWNLOAD.replace("{tagName}", LATEST_TAG_NAME);
                }
            }

//            LATEST_DOWN_URL = LATEST_DOWN_URL.replace("github.com", "192.168.0.254");
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

//        LATEST_TAG_IDENTIFIER = new File(FoldersConsts.TAG_FILE + LATEST_TAG_NAME);
//        if (!LATEST_TAG_IDENTIFIER.exists()) {
//            //有更新
//            return true;
//        }

        System.out.println("Lastest_TagName: " + LATEST_TAG_NAME + ", Current_Jar's_Version: " + currentJarVersion);
        System.out.println("Lastest_Size: " + LATEST_ASSET_SIZE);
        System.out.println("Lastest_ReleaseNotes: \n" + LATEST_DESC);
        System.out.println("Lastest_URL: \n" + LATEST_DOWN_URL);

        if (!LATEST_TAG_NAME.equals(currentJarVersion)) return true;

        return false;
    }

    private static Thread downloadThread;
    private static Thread renderThread;

    private static volatile boolean downloading_mutex = false; //下载过程中不在允许重试下载

    private static volatile boolean stopFlag = false;
    private static volatile boolean isDownloadDone = false;
    private static volatile boolean isRenderDone = false;
    private static volatile int     totalSize = 0;

    private static String getThreadInfo() {
        return getThreadInfo(Thread.currentThread());
    }
    private static String getThreadInfo(Thread t) {
        return t.getName() + "[" + t.getId() + "]-";
    }

    public static void asyncDownloadJar(final InstallAssistFrame frame) {
        //只允许一个线程进入下载
        if (downloading_mutex) return;

        GIT_NOT_CONNECTED_COUNT.set(0);
        stopFlag = false;
        downloading_mutex = true;
        isDownloadDone = false;
        isRenderDone = false;

        final File jarFileOrigin = new File(FoldersConsts.JAR_FILE);
        final File jarFileDownloading  = new File(FoldersConsts.JAR_FILE_DOWN_ING);
        final File jarFileDownloadDone = new File(FoldersConsts.JAR_FILE_DOWN_DONE);

        //删除原始的downloading不完整文件, 如果下载成功过，只要重新下载就删除重新开始
        try {
            jarFileDownloading.delete();
            jarFileDownloadDone.delete();
        } catch (Exception e) {
            System.out.println("Delete Failed:" + FoldersConsts.JAR_FILE_DOWN_ING);
//            throw new RuntimeException(e);
        }

        //独立线程下载
        downloadThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(getThreadInfo() + "-Downloading Starts From " + LATEST_DOWN_URL);
                System.out.println(getThreadInfo() + "-Downloading Saves To " + jarFileDownloading.getAbsolutePath());
//                while (!stopFlag && !isDownloadDone) {
                    //下载文件
                    try {
                        //connectionTimeout - the number of milliseconds until this method will timeout if no connection could be established to the source
                        //readTimeout - the number of milliseconds until this method will timeout if no data could be read from the source
                        FileUtils.copyURLToFile(new URL(LATEST_DOWN_URL), jarFileDownloading, 10*1000, 10*1000);
                        isDownloadDone = true;
                        System.out.println(getThreadInfo() + "-Downloading Done.");
                    } catch (IOException e1) {
                        System.out.println(getThreadInfo() + "-Downloading Failed " + e1.getMessage());
                        e1.printStackTrace();
                        //无需手动终止，render线程会持续查看文件大小，超过次数就当做下载异常处理。
    //                        JiraAutoUpdater.stop();
                    }
//                }
            }
        }, "downloadThread");

        //独立线程更新ProgressBar
        renderThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(getThreadInfo() + "-Check File Size..." + jarFileDownloading.getAbsolutePath());

                while (!stopFlag && !isRenderDone) {
                    Random random = new Random();

                    try {
                        Thread.sleep(1000 + random.nextInt(100));
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }

                    int size = (int) jarFileDownloading.length();
                    int percentage = (int) (size / LATEST_ASSET_SIZE * 100);

                    System.out.println(getThreadInfo() + "-Size is..." + size);
                    System.out.println(getThreadInfo() + "-Progress is " + percentage);

                    frame.renderUI(size, percentage);

                    if (percentage >= 100) {
                        System.out.println(getThreadInfo() + "-Progress Done.");
                        Toolkit.getDefaultToolkit().beep();
                        isRenderDone = true;
                        totalSize = size;

                        //下载完成更改文件名
                        try {
                            Thread.sleep(1000L);

                            //第一步：备份源文件
                            File backupFile = new File(FoldersConsts.JAR_BAK_FILE + System.currentTimeMillis());
                            FileUtils.copyFile(jarFileOrigin, backupFile);
                            System.out.println(getThreadInfo() + "-Backup File From " + jarFileOrigin.getName() + " To " + backupFile.getName());

                            //第二步：新建一个更新文件下载完成的标识新文件。
                            File downloadDoneFile = new File(FoldersConsts.JAR_FILE_DOWN_DONE);
                            FileUtils.touch(downloadDoneFile);
                            System.out.println(getThreadInfo() + "-Touch File " + downloadDoneFile.getName());

                            //第三步：文件更名完毕，才算真正完成，才能enable重启应用按钮
                            frame.renderUI(totalSize, 101);
                        } catch (Exception e1) {
                            throw new RuntimeException(e1);
                        }

                    }
                }

//                System.out.println("Create Tag Identifier File..." + LATEST_TAG_IDENTIFIER.getAbsolutePath());
//                try {
//                    LATEST_TAG_IDENTIFIER.createNewFile();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
            }
        }, "renderThread");


        downloadThread.start();
        renderThread.start();
    }

    public static void stop() {
        stopFlag = true;
        downloading_mutex = false;

        interruptThread(downloadThread);
        interruptThread(renderThread);
    }

    private static void interruptThread(Thread t) {
        try {
            if (t != null) {
                t.interrupt();
                t = null;
            }
        } catch (Exception e) {
            System.out.println("Interrupt thread " + getThreadInfo(t) + " : " + t.isInterrupted());
        }
    }


} public class ExcelProcessor4Sprint {

    public static final String EXCEL_FILE = FoldersConsts.OUTPUT_FOLDER + "/Export-{boardName}-{sprintName}-{date}.xls";
    public static final int    PADDING_LEN    = 10;

    /**
     * 主题ID, hiddenKeys，团队，Tree，类型， 主题Key，
     * 主题，状态，经办人，预估时间(H)，剩余时间(H), 预估时间，剩余时间
     * 项目Key，项目名称，优先级, 描述
     * 创建时间,更新时间,截止时间
     * Browse
     */
    public static short[] SPRINT_COL_WIDTH_PIXEL = new short[] {
            0,      0,      50,    50,     50,     100,
            300,    80,     120,    120,    120,    0,    0,
            0,      0,      120,      0,
            0,      0,      0
    };

    public static final int COL_INIT_NUM = SPRINT_COL_WIDTH_PIXEL.length;

    public static int COL_ADD_NUM  = 0;          //自定义字段的数量
    public static String[] CUSTOM_KEY_ARR;
    public static String[] CUSTOM_NAME_ARR;
    public static Double[] CUSTOM_RECORDS_SUM;    //自定义字段如果是数值型，自动计算一个合计值
    public static boolean CUSTOM_FIELDS_ADDED = false;   //是否已经添加了自定义的列

    //显示所有列 对应环境变量 excelShowAllColumns
    public static short[] SPRINT_COL_WIDTH_PIXEL_ALL = new short[] {
            0,      0,      50,    50,     50,    100,
            300,    80,     120,    120,    120,    100,    100,
            100,    100,    100,    100,
            200,    100,    100
    };

    public static String[] SPRINT_COL_NAME_ARR = new String[] {
            "Id",        "hiddenKeys",   "团队",  "T",   "类型",  "Key",
            "主题",       "状态",       "经办人",   "预估时间(H)",   "剩余时间(H)",  "预估时间",  "剩余时间",
            "项目Key",    "项目名称",    "优先级",    "描述",
            "创建时间",    "更新时间",    "截止时间"
    };

    public static short[] BACKLOG_COL_WIDTH_PIXEL = new short[] {
            0,      0,      50,     50,     50,    60,
            60,     300,    60,     300,
            100,    100,
            300
    };

    public static String[] BACKLOG_COL_NAME_ARR = new String[] {
            "Id",        "hiddenKeys",   "看板", "T",   "类型",  "Key",
            "优先级",    "主题",       "状态",       "描述",
            "创建时间",    "更新时间",
            "备注"
    };

    private static final DecimalFormat DF = new DecimalFormat("#.#");
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static int ROW_IDX_TITLE         = 0;    //标题行号
    public static int ROW_IDX_SUBTITLE      = 0;    //子标题行号
    public static int ROW_IDX_HEADER        = 0;    //列头行号
    public static int ROW_IDX_SUM           = 0;    //汇总行号
    public static int ROW_IDX_DATA          = 0;    //数据起始行号

    public static int COL_IDX_ID            = 0;    //
    public static int COL_IDX_TEAM          = 2;    //
    public static int COL_IDX_TYPE          = 4;    //issue类型列号
    public static int COL_IDX_KEY           = 5;    //
    public static int COL_IDX_STATUS        = 7;    //状态列号
    public static int COL_IDX_OWNER         = 8;    //经办人列号
    public static int COL_IDX_EST_HOUR      = 9;    //估算时间(小时)列号
    public static int COL_IDX_LEFT_HOUR     = 10;    //剩余时间(小时)列号
    public static int COL_IDX_DESC          = 0;     //描述
    public static int COL_IDX_COMMENTS      = 0;     //备注

    public static final List<Integer> TEAM_GROUP_START_ROW = new ArrayList<>();
    public static final List<Integer> STORY_GROUP_START_ROW = new ArrayList<>();
    public static Set<String> PEOPLE = new HashSet<>();//负责人姓名
    public static double TOTAL_ESTIMATE = 0;
    public static double TOTAL_REMAIN   = 0;


    public static String write(String boardId, String sprintId) {
        if (StringUtils.isBlank(boardId))  return null;

        JiraBoard jiraBoard = JIRA.getBoardCache(boardId);
        String boardName = jiraBoard.getBoardName();

        String sprintName = "";

        //1 创建一个workbook对应一个excel文件
        HSSFWorkbook workbook = new HSSFWorkbook();

        if (StringUtils.isNotBlank(sprintId)) {
            JiraSprint jiraSprint = JIRA.getSprintCache(sprintId);

            sprintName = jiraSprint.getSprintName();

            Map<String, JiraIssue> issuesInSprintMap = JIRA.getIssueMapWithSameOrder(boardId, sprintId);
            createSprintSheet(workbook, boardName, sprintName, issuesInSprintMap);
        }

        Map<String, JiraIssue> backlogMap = JIRA.getBacklogIdByJiraUIOrder(boardId);
        createBacklogSheet(workbook, boardName, backlogMap);
        File saveFile = new File(EXCEL_FILE.replace("{boardName}", boardName)
                .replace("{sprintName}", sprintName)
                .replace("{date}", System.currentTimeMillis() + ""));

        //将文件保存到指定的位置
        try {
            FileOutputStream fos = new FileOutputStream(saveFile);
            workbook.write(fos);
            System.out.println("Export Succ!" + saveFile.getAbsolutePath());
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return saveFile.getAbsolutePath();
    }

    private static void _clearData() {
        STORY_GROUP_START_ROW.clear();
        TEAM_GROUP_START_ROW.clear();
        PEOPLE.clear();
        TOTAL_ESTIMATE = 0;
        TOTAL_REMAIN = 0;
    }

    private static void createSprintSheet(HSSFWorkbook workbook, String boardName, String sprintName, Map<String, JiraIssue> issuesInSprintMap) {
        _clearData();

        //将TeamId， 保持issue原序号 拼装为等长字符，实现分组和父子排序
        Map<String, JiraIssue> excelSprintIssue = new TreeMap<String, JiraIssue>();

        int seq = 0;
        for (JiraIssue issue : issuesInSprintMap.values()) {
            seq++;
            // 以防所有的没有子任务的issue都沉到底部，导致team无法分组，仍旧以team分组，
            // 然后再把no subtask的故事或者任务沉到团队内的底部
            excelSprintIssue.put(key4Sort(issue, seq), issue);
        }

        int rowIdx = 0;
        //2 在workbook中创建一个sheet对应excel中的sheet
        HSSFSheet sheet = workbook.createSheet("Sprint");

        //3 标题
        HSSFRow titleRow = sheet.createRow(rowIdx++); ROW_IDX_TITLE = rowIdx - 1;
        titleRow.createCell(0).setCellValue(boardName + "-" + sprintName);
        //子标题
        HSSFRow subtitleRow = sheet.createRow(rowIdx++); ROW_IDX_SUBTITLE = rowIdx - 1;
        subtitleRow.createCell(0).setCellValue(DATE_FORMAT.format(new Date()));

        //4 在sheet表中添加表头，老版本的poi对sheet的行列有限制
        HSSFRow headerRow = sheet.createRow(rowIdx++);  ROW_IDX_HEADER = rowIdx - 1;

        //2018-3-22： 新增自定义的字段，通过json的格式自动拼装到excel里。
        if (!CUSTOM_FIELDS_ADDED) {
            Object keys = PropertiesCache.getProp("excel.customfield.keys");

            if ( keys != null && StringUtils.isNotBlank(keys.toString())) {
                String customKeys = keys.toString();
                String customNames = PropertiesCache.getProp("excel.customfield.names").toString();

                CUSTOM_KEY_ARR = StringUtils.split(customKeys, ",");
                CUSTOM_NAME_ARR = StringUtils.split(customNames, ",");

                if (CUSTOM_KEY_ARR.length != CUSTOM_NAME_ARR.length) {
                    System.out.println("配置都能不一致，你还能干点啥！");
                } else {
                    COL_ADD_NUM = CUSTOM_KEY_ARR.length;
                }

                SPRINT_COL_WIDTH_PIXEL = Arrays.copyOf(SPRINT_COL_WIDTH_PIXEL,      COL_INIT_NUM + COL_ADD_NUM);
                SPRINT_COL_WIDTH_PIXEL_ALL = Arrays.copyOf(SPRINT_COL_WIDTH_PIXEL_ALL,  COL_INIT_NUM + COL_ADD_NUM);
                SPRINT_COL_NAME_ARR = Arrays.copyOf(SPRINT_COL_NAME_ARR,         COL_INIT_NUM + COL_ADD_NUM);

                for (int i = 0; i < COL_ADD_NUM; i++) {
                    SPRINT_COL_WIDTH_PIXEL[COL_INIT_NUM + i] = 100;
                    SPRINT_COL_WIDTH_PIXEL_ALL[COL_INIT_NUM + i] = 100;
                    SPRINT_COL_NAME_ARR[COL_INIT_NUM + i] =  CUSTOM_NAME_ARR[i];
                }
            }

            CUSTOM_FIELDS_ADDED = true;
        }

        //创建单元格，设置表头
        for (int columnIdx = 0; columnIdx < SPRINT_COL_WIDTH_PIXEL.length; columnIdx++) {
            headerRow.createCell(columnIdx).setCellValue(SPRINT_COL_NAME_ARR[columnIdx]);
        }

        //5 汇总行
        HSSFRow sumRow = sheet.createRow(rowIdx++); ROW_IDX_SUM = rowIdx - 1;
        for (int colIdx = 0; colIdx < SPRINT_COL_WIDTH_PIXEL.length; colIdx++) {
            sumRow.createCell(colIdx).setCellValue("");
        }
        //如果自定义字段都是数值，那自动给一个合计值在汇总行
        CUSTOM_RECORDS_SUM = new Double[COL_ADD_NUM];

        //写入实体数据，实际应用中这些数据从数据库得到,对象封装数据，集合包对象。对象的属性值对应表的每行的值
        String previousTeam = null;
        HSSFRow dataRow = null;
        ROW_IDX_DATA = rowIdx;
        for (Map.Entry<String, JiraIssue> entry : excelSprintIssue.entrySet()) {
            //每个故事/任务就是一个分组的开始行
            STORY_GROUP_START_ROW.add(rowIdx);

            String hiddenKey = entry.getKey();
            JiraIssue issue = entry.getValue();

            String currentTeam = StringUtils.trimToEmpty(issue.getCustomFields().get("teamName"));
            if (currentTeam.length() == 0) {
                currentTeam = boardName;
            }

            if (!currentTeam.equalsIgnoreCase(previousTeam)) {
                previousTeam = currentTeam;
                TEAM_GROUP_START_ROW.add(rowIdx);
            }

            dataRow = sheet.createRow(rowIdx++);
            createSprintColumns(dataRow, hiddenKey, issue, currentTeam);

            Map<String, JiraIssue> subTaskMap = issue.getSubTaskMap();
            if (subTaskMap != null && subTaskMap.size() > 0) {
                for (JiraIssue subTask : subTaskMap.values()) {
                    HSSFRow subRow = sheet.createRow(rowIdx++);
                    createSprintColumns(subRow, hiddenKey, subTask, currentTeam);
                }
            }
        }

        renderExcel("Sprint", workbook, sheet, rowIdx - 1, SPRINT_COL_WIDTH_PIXEL.length - 1);

        //写入汇总数据
        sumRow.getCell(COL_IDX_OWNER).setCellValue("总人力: " + PEOPLE.size());
        sumRow.getCell(COL_IDX_EST_HOUR).setCellValue("总估时: " + DF.format(TOTAL_ESTIMATE));
        sumRow.getCell(COL_IDX_LEFT_HOUR).setCellValue("总剩余: " + DF.format(TOTAL_REMAIN));
        for (int i = 0; i < COL_ADD_NUM; i++) {
            Double d = CUSTOM_RECORDS_SUM[i];
            if (d!=null && d > 0.0) {
                sumRow.getCell(COL_INIT_NUM + i).setCellValue(DF.format(d));
            }
        }

        //分组的加减号放到上方
        sheet.setRowSumsBelow(false);
        sheet.setRowSumsRight(false);
    }

    private static void createBacklogSheet(HSSFWorkbook workbook, String boardName, Map<String, JiraIssue> backlogMap) {
        _clearData();

        //将TeamId， 保持issue原序号 拼装为等长字符，实现分组和父子排序
//        Map<String, JiraIssue> excelBacklog = new TreeMap<String, JiraIssue>();
//
//        for (JiraIssue issue : backlogMap.values()) {
//            // 优先级高的靠前，进行中的靠前
//            excelBacklog.put(issue.getSortedKey(), issue);
//        }

        int rowIdx = 0;
        //2 在workbook中创建一个sheet对应excel中的sheet
        HSSFSheet sheet = workbook.createSheet("Backlog");

        //3 标题
        HSSFRow titleRow = sheet.createRow(rowIdx++); ROW_IDX_TITLE = rowIdx - 1;
        titleRow.createCell(0).setCellValue(boardName + " Backlog");
        //子标题
        HSSFRow subtitleRow = sheet.createRow(rowIdx++); ROW_IDX_SUBTITLE = rowIdx - 1;
        subtitleRow.createCell(0).setCellValue(DATE_FORMAT.format(new Date()));

        //4 在sheet表中添加表头，老版本的poi对sheet的行列有限制
        HSSFRow headerRow = sheet.createRow(rowIdx++);  ROW_IDX_HEADER = rowIdx - 1;

        //创建单元格，设置表头
        for (int columnIdx = 0; columnIdx < BACKLOG_COL_WIDTH_PIXEL.length; columnIdx++) {
            headerRow.createCell(columnIdx).setCellValue(BACKLOG_COL_NAME_ARR[columnIdx]);
        }

        //5 汇总行
        HSSFRow sumRow = sheet.createRow(rowIdx++); ROW_IDX_SUM = rowIdx - 1;
        for (int colIdx = 0; colIdx < BACKLOG_COL_WIDTH_PIXEL.length; colIdx++) {
            sumRow.createCell(colIdx).setCellValue("");
        }

        //如果自定义字段都是数值，那自动给一个合计值在汇总行
        CUSTOM_RECORDS_SUM = new Double[COL_ADD_NUM];

        //写入实体数据，实际应用中这些数据从数据库得到,对象封装数据，集合包对象。对象的属性值对应表的每行的值
        HSSFRow dataRow = null;
        ROW_IDX_DATA = rowIdx;
        TEAM_GROUP_START_ROW.add(rowIdx);
        for (Map.Entry<String, JiraIssue> entry : backlogMap.entrySet()) {
            //每个故事/任务就是一个分组的开始行
            STORY_GROUP_START_ROW.add(rowIdx);

            String hiddenKey = entry.getKey();
            JiraIssue issue = entry.getValue();

            dataRow = sheet.createRow(rowIdx++);
            createBacklogColumns(dataRow, hiddenKey, issue);

            if (issue.isHasSubtask()) {
                Map<String, JiraIssue> subTaskMap = issue.getSortedSubTaskMap();

                for (JiraIssue subTask : subTaskMap.values()) {
                    HSSFRow subRow = sheet.createRow(rowIdx++);
                    createBacklogColumns(subRow, hiddenKey, subTask);
                }
            }
        }

        renderExcel("backlog", workbook, sheet, rowIdx - 1, SPRINT_COL_WIDTH_PIXEL.length - 1);

        //分组的加减号放到上方
        sheet.setRowSumsBelow(false);
        sheet.setRowSumsRight(false);
    }

    private static void createBacklogColumns(HSSFRow row, String hiddenKey, JiraIssue issue) {
        int columnIdx = 0;
        row.createCell(columnIdx++).setCellValue(issue.getIssueId());   COL_IDX_ID = columnIdx - 1;
        row.createCell(columnIdx++).setCellValue(hiddenKey);
        row.createCell(columnIdx++).setCellValue(JIRA.getBoardCache(issue.getBoardId()).getBoardName()); COL_IDX_TEAM = columnIdx - 1;
        row.createCell(columnIdx++).setCellValue(renderIssueTypeInTree(issue));
        row.createCell(columnIdx++).setCellValue(issue.getIssueType()); COL_IDX_TYPE = columnIdx - 1;
        Cell linkCell = row.createCell(columnIdx++);
        CreationHelper createHelper = row.getSheet().getWorkbook().getCreationHelper();
        Hyperlink link = createHelper.createHyperlink(HyperlinkType.URL);
        link.setAddress(JIRA.serverUrl + JIRA.BROWSE_ISSUE_URL.replace("{issueKey}", issue.getIssueKey()));
        linkCell.setCellValue(issue.getIssueKey());                     COL_IDX_KEY = columnIdx - 1;
        linkCell.setHyperlink(link);

        row.createCell(columnIdx++).setCellValue(issue.getPriority());
        row.createCell(columnIdx++).setCellValue(issue.getIssueName());
        row.createCell(columnIdx++).setCellValue(issue.getIssueStatus());   COL_IDX_STATUS = columnIdx - 1;
        row.createCell(columnIdx++).setCellValue(issue.getDescription());   COL_IDX_DESC = columnIdx - 1;
        row.createCell(columnIdx++).setCellValue(issue.getCreatedAt());
        row.createCell(columnIdx++).setCellValue(issue.getUpdatedAt());
        row.createCell(columnIdx++).setCellValue(issue.getComments());      COL_IDX_COMMENTS = columnIdx - 1;
    }

    private static void createSprintColumns(HSSFRow row, String hiddenKey, JiraIssue issue, String currentTeam) {
        //创建单元格设值
        int columnIdx = 0;
        row.createCell(columnIdx++).setCellValue(issue.getIssueId());   COL_IDX_ID = columnIdx - 1;
        row.createCell(columnIdx++).setCellValue(hiddenKey);
        row.createCell(columnIdx++).setCellValue(currentTeam);          COL_IDX_TEAM = columnIdx - 1;
        row.createCell(columnIdx++).setCellValue(renderIssueTypeInTree(issue));
        row.createCell(columnIdx++).setCellValue(issue.getIssueType()); COL_IDX_TYPE = columnIdx - 1;

        Cell linkCell = row.createCell(columnIdx++);
        CreationHelper createHelper = row.getSheet().getWorkbook().getCreationHelper();
        Hyperlink link = createHelper.createHyperlink(HyperlinkType.URL);
        link.setAddress(JIRA.serverUrl + JIRA.BROWSE_ISSUE_URL.replace("{issueKey}", issue.getIssueKey()));
        linkCell.setCellValue(issue.getIssueKey());                     COL_IDX_KEY = columnIdx - 1;
        linkCell.setHyperlink(link);

        row.createCell(columnIdx++).setCellValue(issue.getIssueName());
        row.createCell(columnIdx++).setCellValue(issue.getIssueStatus()); COL_IDX_STATUS = columnIdx - 1;

        row.createCell(columnIdx++).setCellValue(issue.getOwner());
        if (StringUtils.isNotBlank(issue.getOwner())) {
            PEOPLE.add(issue.getOwner());
        }

        String est = issue.getEstimate();
        String remain = issue.getRemainingTime();
        String estInSec = issue.getEstimateInSeconds();
        String remainInSec = issue.getRemainingTimeInSeconds();

        if (issue.isSubTask()) {
            String estInHour = "未估算";
            if (StringUtils.isNumeric(estInSec)) {
                //时间转换成小时,该实现是基于估算时间最小颗粒为小时
                double _estinSec = Double.parseDouble(estInSec);

                if (_estinSec > 0 && _estinSec < 1800) {
                    estInHour = "估算不足半小时";
                } else {
                    double d = _estinSec / 3600;
                    estInHour = DF.format(d);
                    TOTAL_ESTIMATE += d;
                }
            }

            //完成任务的剩余时间自动置0
            String remainInHour = "0";
            if (!issue.isDone() && StringUtils.isNumeric(remainInSec)) {
                double _remaininSec = Double.parseDouble(remainInSec);

                if (_remaininSec > 0 && _remaininSec < 1800) {
                } else {
                    double d = _remaininSec / 3600;
                    remainInHour = DF.format(d);
                    TOTAL_REMAIN += d;
                }
            }

            row.createCell(columnIdx++).setCellValue(estInHour);    COL_IDX_EST_HOUR = columnIdx - 1;
            row.createCell(columnIdx++).setCellValue(remainInHour); COL_IDX_LEFT_HOUR = columnIdx - 1;

            row.createCell(columnIdx++).setCellValue(est);
            row.createCell(columnIdx++).setCellValue(remain);
        } else {
            row.createCell(columnIdx++).setCellValue("");
            row.createCell(columnIdx++).setCellValue("");

            row.createCell(columnIdx++).setCellValue("");
            row.createCell(columnIdx++).setCellValue("");
        }

        row.createCell(columnIdx++).setCellValue(issue.getJiraProject().getProjectKey());
        row.createCell(columnIdx++).setCellValue(issue.getJiraProject().getProjectName());
        row.createCell(columnIdx++).setCellValue(issue.getPriority());
        row.createCell(columnIdx++).setCellValue(issue.getDescription()); COL_IDX_DESC = columnIdx - 1;
        row.createCell(columnIdx++).setCellValue(issue.getCreatedAt());
        row.createCell(columnIdx++).setCellValue(issue.getUpdatedAt());
        row.createCell(columnIdx++).setCellValue(issue.getDueDate());

        Map<String, String> customFieldMap = issue.getCustomFields();
        for (int i = 0; i < COL_ADD_NUM; i++) {
            //支持正则表达式的展示 fields>customfield_12101>.*>value
            String customValue = JSONParsingUtil.getValueByExp(customFieldMap,CUSTOM_KEY_ARR[i]);
            row.createCell(columnIdx++).setCellValue(customValue);

            double addedValue = toDouble(customValue);
            if (addedValue != Double.MAX_VALUE) {
                CUSTOM_RECORDS_SUM[i] = (CUSTOM_RECORDS_SUM[i]==null ? 0.0:CUSTOM_RECORDS_SUM[i]);
                CUSTOM_RECORDS_SUM[i] += addedValue;
            }
        }
    }

    private static double toDouble(String s) {
        try {
            return Double.parseDouble(s);
        } catch (Exception e) {
            return Double.MAX_VALUE;
        }
    }

    //判断一行内某些单元格的内容自动换行后是否过高
    private static void setHighRowToDefault(HSSFRow row, short maxHeight, int... colsIdx) {
        boolean isRowHigh = false;
        for (int colIdx : colsIdx) {
            HSSFCell cell = row.getCell(colIdx);
            String cellValue = (cell == null ? "" : cell.getStringCellValue());
            int lines = _getCellLines(cellValue);
            int totalChars = StringUtils.length(cellValue);
            //默认就有4个换行，或者4个每行有25个字符
            if (lines >= 4 || totalChars > 25 * 4) {
                isRowHigh = true;
                break;
            }
        }

        if (isRowHigh) row.setHeightInPoints(maxHeight);
    }

    private static int _getCellLines(String str) {
        try {
            return str.split("\r\n").length;
        } catch (Exception e) {
        }
        return 0;
    }

    //渲染excel
    private static void renderExcel(String type, HSSFWorkbook workbook, HSSFSheet sheet, int rowCount, int colCount) {
        boolean isBacklogSheet = StringUtils.equalsIgnoreCase("backlog", type);


        short[] col_width_pixel = SPRINT_COL_WIDTH_PIXEL;
        short[] col_width_pixel_all = SPRINT_COL_WIDTH_PIXEL_ALL;
        if (isBacklogSheet) {
            col_width_pixel = BACKLOG_COL_WIDTH_PIXEL;
            col_width_pixel_all = BACKLOG_COL_WIDTH_PIXEL;
        }

        //合并标题列
        sheet.addMergedRegion(new CellRangeAddress(ROW_IDX_TITLE,ROW_IDX_TITLE,0, col_width_pixel.length - 1));
        sheet.getRow(ROW_IDX_TITLE).setHeightInPoints(40);

        sheet.addMergedRegion(new CellRangeAddress(ROW_IDX_SUBTITLE,ROW_IDX_SUBTITLE,0, col_width_pixel.length - 1));
        sheet.getRow(ROW_IDX_SUBTITLE).setHeightInPoints(20);

        sheet.getRow(ROW_IDX_HEADER).setHeightInPoints(35);

        //冰冻抬头
        sheet.createFreezePane(COL_IDX_STATUS, ROW_IDX_DATA);

        //隐藏hiddenKeys列, 其他列自动调整
//        sheet.setColumnHidden(0, true);

        String excelColumns = PropertiesCache.getProp("excelShowAllColumns");

        for (int i = 0; i <= colCount; i++){
            if (i < col_width_pixel.length) {
                int wu = 0;
                if ("1".equals(excelColumns)) {
                    wu = ExcelUtils.pixel2WidthUnits(col_width_pixel_all[i]);
                } else {
                    wu = ExcelUtils.pixel2WidthUnits(col_width_pixel[i]);
                }

                sheet.setColumnWidth(i, wu);//自动调整列宽度
            } else {
                sheet.autoSizeColumn(i);
            }
        }

        short contentSize = (short)(isBacklogSheet? 9 : 12);

        //标题样式
        HSSFCellStyle titleStyle = workbook.createCellStyle();
        HSSFFont titleFont = workbook.createFont();
        titleFont.setFontName("微软雅黑");
        titleFont.setFontHeightInPoints((short) 24);//设置字体大小
        titleFont.setBold(true);
        titleStyle.setFont(titleFont);
        titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        titleStyle.setAlignment(HorizontalAlignment.CENTER);

        //子标题样式
        HSSFCellStyle subtitleStyle = workbook.createCellStyle();
        HSSFFont subtitleFont = workbook.createFont();
        subtitleFont.setFontName("微软雅黑");
        subtitleFont.setFontHeightInPoints((short) 14);//设置字体大小
        subtitleFont.setBold(false);
        subtitleStyle.setFont(subtitleFont);
        subtitleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        subtitleStyle.setAlignment(HorizontalAlignment.RIGHT);

        //边框样式
        HSSFCellStyle borderStyle = workbook.createCellStyle();
        borderStyle.setBorderBottom(BorderStyle.THIN);    //下边框
        borderStyle.setBorderLeft(BorderStyle.THIN);      //左边框
        borderStyle.setBorderTop(BorderStyle.THIN);       //上边框
        borderStyle.setBorderRight(BorderStyle.THIN);     //右边框
        borderStyle.setVerticalAlignment(VerticalAlignment.CENTER);

        //Team字体样式
        HSSFCellStyle teamStyle = workbook.createCellStyle();
        teamStyle.cloneStyleFrom(borderStyle);
        teamStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        teamStyle.setAlignment(HorizontalAlignment.CENTER);
        HSSFFont teamFont = workbook.createFont();
        teamFont.setFontName("微软雅黑");
        teamFont.setFontHeightInPoints((short) 14);//设置字体大小
        teamFont.setBold(true);
        teamStyle.setFont(teamFont);
        teamStyle.setRotation((short)255);

        //列头字体样式
        HSSFCellStyle headerStyle = workbook.createCellStyle();
        headerStyle.cloneStyleFrom(borderStyle);
        HSSFFont headerFont = workbook.createFont();
        headerFont.setFontName("微软雅黑");
        headerFont.setFontHeightInPoints((short) 16);//设置字体大小
        headerFont.setBold(true);
        headerStyle.setFont(headerFont);  //选择需要用到的字体格式
        headerStyle.setAlignment(HorizontalAlignment.CENTER);
//        headerStyle.setFillBackgroundColor(HSSFColor.HSSFColorPredefined.LIGHT_YELLOW.getIndex());
        headerStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_YELLOW.getIndex());
        headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //汇总字体样式
        HSSFCellStyle sumStyle = workbook.createCellStyle();
        sumStyle.cloneStyleFrom(headerStyle);
        sumStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.WHITE.getIndex()); //去除背景色
        sumStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //内容字体样式
        HSSFCellStyle issueStyle = workbook.createCellStyle();
        issueStyle.cloneStyleFrom(borderStyle);
        HSSFFont issueFont = workbook.createFont();
        issueFont.setFontName("微软雅黑");
        issueFont.setFontHeightInPoints(contentSize);//设置字体大小
        issueStyle.setFont(issueFont);      //选择需要用到的字体格式
        issueStyle.setAlignment(HorizontalAlignment.LEFT);
        HSSFDataFormat df = workbook.createDataFormat();
        issueStyle.setDataFormat(df.getFormat("@"));    //设置文本格式
        if (isBacklogSheet) {
            issueStyle.setWrapText(true);
        }

        //类型 = 故事/任务 加粗
        HSSFCellStyle storyOrTaskStyle = workbook.createCellStyle();
        storyOrTaskStyle.cloneStyleFrom(issueStyle);
        HSSFFont storyOrTaskFont = workbook.createFont();
        storyOrTaskFont.setBold(true);
        storyOrTaskFont.setFontName("微软雅黑");
        storyOrTaskFont.setFontHeightInPoints(contentSize);//设置字体大小
        storyOrTaskStyle.setFont(storyOrTaskFont);      //选择需要用到的字体格式
        storyOrTaskStyle.setAlignment(HorizontalAlignment.LEFT);
        storyOrTaskStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_CORNFLOWER_BLUE.getIndex());
        storyOrTaskStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //状态颜色
        HSSFCellStyle todoStyle = workbook.createCellStyle();
        todoStyle.cloneStyleFrom(issueStyle);
        todoStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_ORANGE.getIndex());
        todoStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        HSSFCellStyle doingStyle = workbook.createCellStyle();
        doingStyle.cloneStyleFrom(issueStyle);
        doingStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_GREEN.getIndex());
        doingStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        HSSFCellStyle doneStyle = workbook.createCellStyle();
        doneStyle.cloneStyleFrom(issueStyle);
        doneStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.GREY_40_PERCENT.getIndex());
        doneStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //链接样式
        CellStyle linkStyle4Issue = workbook.createCellStyle();
        linkStyle4Issue.cloneStyleFrom(issueStyle);
        Font hlink_font = workbook.createFont();
        hlink_font.setFontName("微软雅黑");
        hlink_font.setFontHeightInPoints(contentSize);
        hlink_font.setUnderline(Font.U_SINGLE);
        hlink_font.setColor(IndexedColors.BLACK.getIndex());
        linkStyle4Issue.setFont(hlink_font);
        linkStyle4Issue.setAlignment(HorizontalAlignment.CENTER);

        CellStyle linkStyle4Story = workbook.createCellStyle();
        linkStyle4Story.cloneStyleFrom(issueStyle);
        Font hlink_font_story = workbook.createFont();
        hlink_font_story.setFontName("微软雅黑");
        hlink_font_story.setFontHeightInPoints(contentSize);
        hlink_font_story.setUnderline(Font.U_SINGLE);
        hlink_font_story.setColor(IndexedColors.BLACK.getIndex());
        linkStyle4Story.setFont(hlink_font_story);
        linkStyle4Story.setAlignment(HorizontalAlignment.CENTER);
        linkStyle4Story.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_CORNFLOWER_BLUE.getIndex());
        linkStyle4Story.setFillPattern(FillPatternType.SOLID_FOREGROUND);


        for (int row = 0; row <= rowCount; row++) {
            HSSFRow excelRow = sheet.getRow(row);

            JiraIssue jiraIssue = null;
            for(int col = 0; col <= colCount; col++) {
                HSSFCell excelCell = excelRow.getCell(col);

                if (excelCell == null) break;

                if (row > ROW_IDX_SUM && col == COL_IDX_ID) {
                    String issueId = excelCell.getStringCellValue();
                    jiraIssue = JIRA.getIssueCached(issueId);
                }

                excelCell.setCellStyle(borderStyle);
                if (row == ROW_IDX_TITLE) {
                    excelCell.setCellStyle(titleStyle);
                } else if (row == ROW_IDX_SUBTITLE) {
                    excelCell.setCellStyle(subtitleStyle);
                } else if (row == ROW_IDX_HEADER) {
                    excelCell.setCellStyle(headerStyle);
                } else if (row == ROW_IDX_SUM) {
                    excelCell.setCellStyle(sumStyle);
                } else if (col == COL_IDX_STATUS && jiraIssue.isSubTask()) {
                    if (jiraIssue.isTodo()) {
                        excelCell.setCellStyle(todoStyle);
                    } else if (jiraIssue.isDoing()) {
                        excelCell.setCellStyle(doingStyle);
                    } else if (jiraIssue.isDone()) {
                        excelCell.setCellStyle(doneStyle);
                    }
                } else if (jiraIssue.isParent()){
                    if (isBacklogSheet && col >= COL_IDX_DESC) {
                        //backlog里的描述备注不需要使用故事的加粗和着色
                        excelCell.setCellStyle(issueStyle);
                    } else {
                        excelCell.setCellStyle(storyOrTaskStyle);
                    }
                } else {
                    excelCell.setCellStyle(issueStyle);
                }

                if (excelCell.getHyperlink() != null) {
                    if (jiraIssue.isParent()) {
                        excelCell.setCellStyle(linkStyle4Story);
                    } else {
                        excelCell.setCellStyle(linkStyle4Issue);
                    }
                }

            }
        }

        //若行数太多，以防自动换行表太长，设置一个最大高度。
        if (isBacklogSheet) {
            for (int rowIdx = ROW_IDX_DATA; rowIdx <= rowCount; rowIdx++) {
                HSSFRow excelRow = sheet.getRow(rowIdx);
                setHighRowToDefault(excelRow, (short)60, COL_IDX_DESC, COL_IDX_COMMENTS);
            }
        }

        //合并分组功能报错，无视。
        try {
            //Team分组
            for (int i = 0; i < TEAM_GROUP_START_ROW.size(); i++) {
                int groupStart = TEAM_GROUP_START_ROW.get(i);
                int groupEnd  = 0;
                if (i + 1 >= TEAM_GROUP_START_ROW.size()) {
                    groupEnd = rowCount;
                } else {
                    groupEnd = TEAM_GROUP_START_ROW.get(i + 1) - 1;
                }

                if (groupStart < groupEnd) {
                    sheet.groupRow(groupStart + 1, groupEnd);       //Otherwise group会粘连
                }
                //多于1行的合并才有意义
                if (groupStart < groupEnd) {
                    CellRangeAddress region = new CellRangeAddress(groupStart, groupEnd, COL_IDX_TEAM, COL_IDX_TEAM);
                    sheet.addMergedRegion(region);
                }

                HSSFRow regionRow = sheet.getRow(groupStart);
                if (regionRow != null) {
                    HSSFCell regionCell = regionRow.getCell(COL_IDX_TEAM);
                    regionCell.setCellStyle(teamStyle);
                }

                //HSSFRegionUtil;
                //RegionUtil
                //HSSFRegionUtil
            }

            //Story分组
            for (int i = 0; i < STORY_GROUP_START_ROW.size(); i++) {
                int groupStart = STORY_GROUP_START_ROW.get(i);
                int groupEnd  = 0;
                if (i + 1 >= STORY_GROUP_START_ROW.size()) {
                    groupEnd = rowCount;
                } else {
                    groupEnd = STORY_GROUP_START_ROW.get(i + 1) - 1;
                }
                if (groupStart < groupEnd) {
                    sheet.groupRow(groupStart + 1, groupEnd);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static String renderIssueTypeInTree(JiraIssue jiraIssue) {
        String issueType = jiraIssue.getIssueType();
        if (jiraIssue.isParent()) {
            return "|- ";
        } else if (jiraIssue.isSubTask()) {
            return "|----- ";
        }

        return issueType;
    }

    public static void read(String excelPath) {
        File excelFile = new File(excelPath);

        POIFSFileSystem fileSystem = null;
        HSSFWorkbook workbook = null;

        try {
            fileSystem = new POIFSFileSystem(new BufferedInputStream(new FileInputStream(excelFile)));
            workbook = new HSSFWorkbook(fileSystem);
            HSSFSheet sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());

            int lastRowIndex = sheet.getLastRowNum();
            for (int i = 1; i <= lastRowIndex; i++) {
                HSSFRow row = sheet.getRow(i);

                short lastCellNum = row.getLastCellNum();

//                StringBuffer sb = new StringBuffer();
//                sb.append("COLOR_").append(_getCellValue(row.getCell(0)).toUpperCase()).append(" ")
//                        .append("(\"").append(_getCellValue(row.getCell(0)).toUpperCase()).append("\",")
//                        .append("\"").append(_getCellValue(row.getCell(1))).append("\",")
//                        .append("\"").append(_getCellValue(row.getCell(2))).append("\"),")
//                        ;
//
//                System.out.println(sb.toString());
//                for (int j = 0; j < lastCellNum; j++) {
//                    HSSFCell cell = row.getCell(j);
//                    String cellValue = _getCellValue(cell);
//
//
//                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                workbook.close();
            } catch (IOException e) {
            }
            try {
                fileSystem.close();
            } catch (IOException e) {
            }
        }
    }

    private static String key4Sort(JiraIssue jiraIssue, int seq) {
        return StringUtils.leftPad(jiraIssue.getCustomFields().get("teamId"), PADDING_LEN, "0") +
                StringUtils.leftPad(String.valueOf(seq), PADDING_LEN, "0");
    }

    private static String key4SortBacklog(JiraIssue jiraIssue, int seq) {
        return jiraIssue.getPriority4Sort() +
                jiraIssue.getStatusCategory4Sort() +
                StringUtils.leftPad(String.valueOf(seq), PADDING_LEN, "0");
    }

    private static boolean isSameTeam(String hiddenKey1, String hiddenKey2) {
        return getTeamId(hiddenKey1).equals(getTeamId(hiddenKey2));
    }

    private static String getTeamId(String hiddenKey) {
        return StringUtils.substring(hiddenKey, 0, PADDING_LEN);
    }

    private static String getParentGroup(String hiddenKey) {
        return StringUtils.substring(hiddenKey, 0, PADDING_LEN * 2);
    }

    private static boolean _isCellBlank(HSSFCell cell) {
        String s = _getCellValue(cell);
        return s == null || s.trim().length() == 0;
    }

    private static String _getCellValue(HSSFCell cell) {
        if (cell==null) {
            return "";
        }
        String cellValue = null;

        FormulaEvaluator evaluator = cell.getSheet().getWorkbook().getCreationHelper().createFormulaEvaluator();

        CellType type = cell.getCellTypeEnum();

        if (CellType.STRING == type) {
            cellValue = cell.getStringCellValue().trim();
        } else if (CellType.NUMERIC == type) {
            cellValue = String.valueOf(cell.getNumericCellValue()).trim();
        } else if (CellType.BOOLEAN == type) {
            cellValue = String.valueOf(cell.getBooleanCellValue()).trim();
        } else if (CellType.FORMULA == type) {
            cellValue = evaluator.evaluateInCell(cell).toString().trim();
        } else {
            cellValue = "";
        }

        return cellValue;
    }


    public static void main(String[] args) {
        System.out.println(StringUtils.substring("000000009500000154830000015483", 0, PADDING_LEN));
        System.out.println(StringUtils.substring("000000009500000154830000015483", PADDING_LEN, PADDING_LEN*2));
    }


} public class VPLViaExcel extends ViewOfProductLine{

    public static final String EXCEL_FILE = FoldersConsts.OUTPUT_FOLDER + "/OQPL-{date}.xls";

    public static final int COL_INIT_NUM = COL_WIDTH_PIXEL.length;

    //显示所有列 对应环境变量 excelShowAllColumns
    public static short[] COL_WIDTH_PIXEL_ALL = COL_WIDTH_PIXEL;

    public static int COL_ADD_NUM  = 0;          //自定义字段的数量
    public static String[] CUSTOM_KEY_ARR;
    public static String[] CUSTOM_NAME_ARR;
    public static Double[] CUSTOM_RECORDS_SUM;    //自定义字段如果是数值型，自动计算一个合计值
    public static boolean CUSTOM_FIELDS_ADDED = false;   //是否已经添加了自定义的列

    public static int ROW_IDX_TITLE         = 0;    //标题行号
    public static int ROW_IDX_SUBTITLE      = 0;    //子标题行号
    public static int ROW_IDX_HEADER        = 0;    //列头行号
    public static int ROW_IDX_SUM           = 0;    //汇总行号
    public static int ROW_IDX_DATA          = 0;    //数据起始行号

    public static int COL_IDX_PRODUCT       = 0;    //产品名称
    public static int COL_IDX_PD_OWNER      = 1;    //产品负责人
    public static int COL_IDX_PD_PRIORITY   = 2;    //产品优先级
    public static int COL_IDX_PD_STATUS     = 3;    //产品状态
    public static int COL_IDX_PD_TOTAL_WORK = 4;    //产品总工时
    public static int COL_IDX_PD_TOTAL_COST = 5;    //产品总成本
    public static int COL_IDX_PD_REVENUE    = 6;    //产品收益情况
    public static int COL_IDX_PJ_NAME       = 7;    //所属迭代或者泳道

    public static int COL_IDX_US_STATUS     = 11;   //故事状态

    public static final List<Integer> PRODUCT_GROUP_START_ROW = new ArrayList<>();
    public static final List<Integer> PROJECT_GROUP_START_ROW = new ArrayList<>();

    public static String generate(Map<String, Map<String, JiraIssue>> epicIssuesMap) {
        if (epicIssuesMap == null || epicIssuesMap.size() == 0)  return null;

        PROJECT_GROUP_START_ROW.clear();
        PRODUCT_GROUP_START_ROW.clear();

        //解析数据，输入易于HTML排版的数据二维信息
        sortIssues(epicIssuesMap);

        //1 创建一个workbook对应一个excel文件
        HSSFWorkbook workbook = new HSSFWorkbook();

        renderExcel(workbook);

        File saveFile = new File(EXCEL_FILE.replace("{date}", DATE_FORMAT_SLIM.format(new Date())));

        //将文件保存到指定的位置
        try {
            FileOutputStream fos = new FileOutputStream(saveFile);
            workbook.write(fos);
            System.out.println("View OOQI ProductLines Succ!" + saveFile.getAbsolutePath());
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return saveFile.getAbsolutePath();
    }

    private static void renderExcel(HSSFWorkbook workbook) {
        int rowIdx = 0;

        //2 在workbook中创建一个sheet对应excel中的sheet
        HSSFSheet sheet = workbook.createSheet("ViewOfProductLine");

        //3 标题
        HSSFRow titleRow = sheet.createRow(rowIdx++); ROW_IDX_TITLE = rowIdx - 1;
        titleRow.createCell(0).setCellValue(TITLE);
        //子标题
        HSSFRow subtitleRow = sheet.createRow(rowIdx++); ROW_IDX_SUBTITLE = rowIdx - 1;
        subtitleRow.createCell(0).setCellValue(DATE_FORMAT.format(new Date()));

        //4 在sheet表中添加表头，老版本的poi对sheet的行列有限制
        HSSFRow headerRow = sheet.createRow(rowIdx++);  ROW_IDX_HEADER = rowIdx - 1;

        //创建单元格，设置表头
        for (int columnIdx = 0; columnIdx < COL_WIDTH_PIXEL.length; columnIdx++) {
            headerRow.createCell(columnIdx).setCellValue(new HSSFRichTextString(COL_NAME_ARR[columnIdx].replaceAll("<br/>","\r\n")));
        }

        //5 汇总行
        HSSFRow sumRow = sheet.createRow(rowIdx++); ROW_IDX_SUM = rowIdx - 1;
        for (int colIdx = 0; colIdx < COL_WIDTH_PIXEL.length; colIdx++) {
            sumRow.createCell(colIdx).setCellValue("");
        }

        //6 写入行数据
        HSSFRow dataRow = null;
        ROW_IDX_DATA = rowIdx;
        for (String combinedKey : SORTED_ISSUES_MAP.keySet()) {
            JiraIssue jiraIssue = SORTED_ISSUES_MAP.get(combinedKey);

            dataRow = sheet.createRow(rowIdx++);

            createColumns(dataRow, jiraIssue.getJiraEpic(), jiraIssue);
        }

        //写入汇总数据

        //分组的加减号放到上方
        sheet.setRowSumsBelow(false);
        sheet.setRowSumsRight(false);

        renderStyle(workbook, sheet, rowIdx - 1, COL_WIDTH_PIXEL.length - 1);
    }

    private static void createColumns(HSSFRow row, JiraEpic jiraEpic, JiraIssue jiraIssue) {
        //创建单元格设值
        int columnIdx = 0;

        createCellWithLink(row, columnIdx++, jiraEpic.getEpicName(), jiraEpic.getEpicKey());
        row.createCell(columnIdx++).setCellValue(jiraEpic.getEpicOwner());
        row.createCell(columnIdx++).setCellValue(jiraEpic.getCustomPriority());
        row.createCell(columnIdx++).setCellValue(jiraEpic.getEpicStatus());
        row.createCell(columnIdx++).setCellValue(jiraEpic.getCustomTotalWorking());
        row.createCell(columnIdx++).setCellValue(jiraEpic.getCost());
        row.createCell(columnIdx++).setCellValue(jiraEpic.getCustomRevenueInfo());

        String[] displayProject = getDisplayProject(jiraEpic, jiraIssue);
        String projectKey  = displayProject[0];
        String projectName =  displayProject[1];
        createCellWithLink(row, columnIdx++, projectName, projectKey);

        createCellWithLink(row, columnIdx++, jiraIssue.getIssueName(), jiraIssue.getIssueKey());
        row.createCell(columnIdx++).setCellValue(jiraIssue.getOwner());
        row.createCell(columnIdx++).setCellValue(jiraIssue.getPriority());
        row.createCell(columnIdx++).setCellValue(jiraIssue.getIssueStatus());

        row.createCell(columnIdx++).setCellValue(jiraIssue.getDescription());
        row.createCell(columnIdx++).setCellValue(jiraIssue.getCustomEstimateDone());
        row.createCell(columnIdx++).setCellValue(jiraIssue.getCustomActualDone());
        row.createCell(columnIdx++).setCellValue(jiraIssue.getUpdatedAt());


    }

    private static void createCellWithLink(HSSFRow row, int columnIdx, String linkName, String issueKey) {
        Cell linkCell = row.createCell(columnIdx);

        CreationHelper createHelper = row.getSheet().getWorkbook().getCreationHelper();
        Hyperlink link = createHelper.createHyperlink(HyperlinkType.URL);

        if (StringUtils.isNotBlank(issueKey)) {
            link.setAddress(JIRA.serverUrl + JIRA.BROWSE_ISSUE_URL.replace("{issueKey}", issueKey));
        } else {
            link.setAddress(JIRA.serverUrl);
        }
        linkCell.setCellValue(linkName);
        linkCell.setHyperlink(link);
    }

    //渲染excel
    private static void renderStyle(HSSFWorkbook workbook, HSSFSheet sheet, int rowCount, int colCount) {
        //合并标题列
        sheet.addMergedRegion(new CellRangeAddress(ROW_IDX_TITLE,ROW_IDX_TITLE,0,COL_WIDTH_PIXEL.length - 1));
        sheet.getRow(ROW_IDX_TITLE).setHeightInPoints(40);

        sheet.addMergedRegion(new CellRangeAddress(ROW_IDX_SUBTITLE,ROW_IDX_SUBTITLE,0,COL_WIDTH_PIXEL.length - 1));
        sheet.getRow(ROW_IDX_SUBTITLE).setHeightInPoints(20);

        sheet.getRow(ROW_IDX_HEADER).setHeightInPoints(40);

        //隐藏hiddenKeys列, 其他列自动调整
//        sheet.setColumnHidden(0, true);

        String excelColumns = PropertiesCache.getProp("excelShowAllColumns");
        for (int i = 0; i <= colCount; i++){
            if (i < COL_WIDTH_PIXEL.length) {
                int wu = 0;
                if ("1".equals(excelColumns)) {
                    wu = ExcelUtils.pixel2WidthUnits(COL_WIDTH_PIXEL_ALL[i]);
                } else {
                    wu = ExcelUtils.pixel2WidthUnits(COL_WIDTH_PIXEL[i]);
                }

                sheet.setColumnWidth(i, wu);//自动调整列宽度
            } else {
                sheet.autoSizeColumn(i);
            }
        }

        //标题样式
        HSSFCellStyle titleStyle = workbook.createCellStyle();
        HSSFFont titleFont = workbook.createFont();
        titleFont.setFontName("微软雅黑");
        titleFont.setFontHeightInPoints((short) 20);//设置字体大小
        titleFont.setBold(true);
        titleStyle.setFont(titleFont);
        titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        titleStyle.setAlignment(HorizontalAlignment.CENTER);
        //子标题样式
        HSSFCellStyle subtitleStyle = workbook.createCellStyle();
        HSSFFont subtitleFont = workbook.createFont();
        subtitleFont.setFontName("微软雅黑");
        subtitleFont.setFontHeightInPoints((short) 10);//设置字体大小
        subtitleFont.setBold(false);
        subtitleStyle.setFont(subtitleFont);
        subtitleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        subtitleStyle.setAlignment(HorizontalAlignment.RIGHT);

        //边框样式
        HSSFCellStyle borderStyle = workbook.createCellStyle();
        borderStyle.setBorderBottom(BorderStyle.THIN);    //下边框
        borderStyle.setBorderLeft(BorderStyle.THIN);      //左边框
        borderStyle.setBorderTop(BorderStyle.THIN);       //上边框
        borderStyle.setBorderRight(BorderStyle.THIN);     //右边框
        borderStyle.setVerticalAlignment(VerticalAlignment.CENTER);

        //PRODUCT字体样式
        HSSFCellStyle productStyle = workbook.createCellStyle();
        productStyle.cloneStyleFrom(borderStyle);
        productStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        productStyle.setAlignment(HorizontalAlignment.CENTER);
        HSSFFont teamFont = workbook.createFont();
        teamFont.setFontName("微软雅黑");
        teamFont.setFontHeightInPoints((short) 11);//设置字体大小
        teamFont.setBold(true);
        productStyle.setFont(teamFont);

        //列头字体样式
        HSSFCellStyle headerStyle = workbook.createCellStyle();
        headerStyle.cloneStyleFrom(borderStyle);
        HSSFFont headerFont = workbook.createFont();
        headerFont.setFontName("微软雅黑");
        headerFont.setFontHeightInPoints((short) 11);//设置字体大小
        headerFont.setBold(true);
        headerStyle.setWrapText(true); //列头可以换行
        headerStyle.setFont(headerFont);  //选择需要用到的字体格式
        headerStyle.setAlignment(HorizontalAlignment.CENTER);
//        headerStyle.setFillBackgroundColor(HSSFColor.HSSFColorPredefined.LIGHT_YELLOW.getIndex());
        headerStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_YELLOW.getIndex());
        headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //汇总字体样式
        HSSFCellStyle sumStyle = workbook.createCellStyle();
        sumStyle.cloneStyleFrom(headerStyle);
        sumStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.WHITE.getIndex()); //去除背景色
        sumStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //内容字体样式
        HSSFCellStyle issueStyle = workbook.createCellStyle();
        issueStyle.cloneStyleFrom(borderStyle);
        HSSFFont issueFont = workbook.createFont();
        issueFont.setFontName("微软雅黑");
        issueFont.setFontHeightInPoints((short) 10);//设置字体大小
        issueStyle.setFont(issueFont);      //选择需要用到的字体格式
        issueStyle.setAlignment(HorizontalAlignment.LEFT);
        HSSFDataFormat df = workbook.createDataFormat();
        issueStyle.setDataFormat(df.getFormat("@"));    //设置文本格式
//        issueStyle.setWrapText(true);

        //类型 = 故事/任务 加粗
        HSSFCellStyle storyOrTaskStyle = workbook.createCellStyle();
        storyOrTaskStyle.cloneStyleFrom(issueStyle);
        HSSFFont storyOrTaskFont = workbook.createFont();
        storyOrTaskFont.setBold(true);
        storyOrTaskFont.setFontName("微软雅黑");
        storyOrTaskFont.setFontHeightInPoints((short) 10);//设置字体大小
        storyOrTaskStyle.setFont(storyOrTaskFont);      //选择需要用到的字体格式
        storyOrTaskStyle.setAlignment(HorizontalAlignment.LEFT);
        storyOrTaskStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_CORNFLOWER_BLUE.getIndex());
        storyOrTaskStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //状态颜色
        HSSFCellStyle todoStyle = workbook.createCellStyle();
        todoStyle.cloneStyleFrom(issueStyle);
        todoStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_ORANGE.getIndex());
        todoStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        HSSFCellStyle doingStyle = workbook.createCellStyle();
        doingStyle.cloneStyleFrom(issueStyle);
        doingStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.LIGHT_GREEN.getIndex());
        doingStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        HSSFCellStyle doneStyle = workbook.createCellStyle();
        doneStyle.cloneStyleFrom(issueStyle);
        doneStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.GREY_40_PERCENT.getIndex());
        doneStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //链接样式
        CellStyle linkStyle = workbook.createCellStyle();
        linkStyle.cloneStyleFrom(borderStyle);
        Font hlink_font = workbook.createFont();
        hlink_font.setFontName("微软雅黑");
        hlink_font.setFontHeightInPoints((short) 10);
        hlink_font.setUnderline(Font.U_SINGLE);
        hlink_font.setColor(IndexedColors.BLUE.getIndex());
        linkStyle.setFont(hlink_font);

        List<JiraIssue> jiraIssueList = new ArrayList<>();
        jiraIssueList.addAll(SORTED_ISSUES_MAP.values());

        for (int row = 0; row <= rowCount; row++) {
            HSSFRow excelRow = sheet.getRow(row);

            if (row >= ROW_IDX_DATA) {
                JiraIssue jiraIssue = jiraIssueList.get(row - ROW_IDX_DATA);
                if (jiraIssue != null) {

                    HSSFCell pdStatusCell = excelRow.getCell(COL_IDX_PD_STATUS);
                    if (jiraIssue.getJiraEpic().isTodo()) {
                        pdStatusCell.setCellStyle(todoStyle);
                    } else if (jiraIssue.getJiraEpic().isDoing()) {
                        pdStatusCell.setCellStyle(doingStyle);
                    } else if (jiraIssue.getJiraEpic().isDone()) {
                        pdStatusCell.setCellStyle(doneStyle);
                    }

                    HSSFCell usStatusCell = excelRow.getCell(COL_IDX_US_STATUS);
                    if (jiraIssue.isTodo()) {
                        usStatusCell.setCellStyle(todoStyle);
                    } else if (jiraIssue.isDoing()) {
                        usStatusCell.setCellStyle(doingStyle);
                    } else if (jiraIssue.isDone()) {
                        usStatusCell.setCellStyle(doneStyle);
                    }
                }
            }

            for(int col = 0; col <= colCount; col++) {
                HSSFCell excelCell = excelRow.getCell(col);

                if (excelCell == null) break;

                excelCell.setCellStyle(borderStyle);
                if (row == ROW_IDX_TITLE) {
                    excelCell.setCellStyle(titleStyle);
                } else if (row == ROW_IDX_SUBTITLE) {
                    excelCell.setCellStyle(subtitleStyle);
                } else if (row == ROW_IDX_HEADER) {
                    excelCell.setCellStyle(headerStyle);
                } else if (row == ROW_IDX_SUM) {
                    excelCell.setCellStyle(sumStyle);
                } else {
                    excelCell.setCellStyle(issueStyle);
                }

                if (excelCell.getHyperlink() != null) {
                    excelCell.setCellStyle(linkStyle);
                }
            }
        }

        //将需要merge需要分组的行序号摘取出来
        String previousEpicGroupKey = "";
        String previousProjectGroupKey = "";

        String currEpicGroupKey = "";
        String currProjectGroupKey = "";

        int groupRowIdx = ROW_IDX_DATA;
        for (String combinedKey : SORTED_ISSUES_MAP.keySet()) {
            currEpicGroupKey = getEpicGroupKey(combinedKey);
            currProjectGroupKey = getEpicProjectGroupKey(combinedKey);

            //新的Epic
            if (!currEpicGroupKey.equals(previousEpicGroupKey)) {
                previousEpicGroupKey = currEpicGroupKey;
                PRODUCT_GROUP_START_ROW.add(groupRowIdx);
            }

            //新的Project
            if (!getEpicProjectGroupKey(combinedKey).equals(previousProjectGroupKey)) {
                previousProjectGroupKey = currProjectGroupKey;
                PROJECT_GROUP_START_ROW.add(groupRowIdx);
            }
            groupRowIdx++;
        }

        //Project分组
        for (int i = 0; i < PROJECT_GROUP_START_ROW.size(); i++) {
            int groupStart = PROJECT_GROUP_START_ROW.get(i);
            int groupEnd  = 0;
            if (i + 1 >= PROJECT_GROUP_START_ROW.size()) {
                groupEnd = rowCount;
            } else {
                groupEnd = PROJECT_GROUP_START_ROW.get(i + 1) - 1;
            }
            if (groupStart < groupEnd) {
                groupRows(sheet, groupStart + 1, groupEnd);

                mergeRows(sheet, groupStart, groupEnd, COL_IDX_PJ_NAME, COL_IDX_PJ_NAME);
            }
        }

        //产品分组
        for (int i = 0; i < PRODUCT_GROUP_START_ROW.size(); i++) {
            int groupStart = PRODUCT_GROUP_START_ROW.get(i);
            int groupEnd  = 0;
            if (i + 1 >= PRODUCT_GROUP_START_ROW.size()) {
                groupEnd = rowCount;
            } else {
                groupEnd = PRODUCT_GROUP_START_ROW.get(i + 1) - 1;
            }

            try {

                if (groupStart < groupEnd) {

                    groupRows(sheet, groupStart + 1, groupEnd);

                    mergeRows(sheet, groupStart, groupEnd, COL_IDX_PRODUCT, COL_IDX_PRODUCT);
                    mergeRows(sheet, groupStart, groupEnd, COL_IDX_PD_OWNER, COL_IDX_PD_OWNER);
                    mergeRows(sheet, groupStart, groupEnd, COL_IDX_PD_PRIORITY, COL_IDX_PD_PRIORITY);
                    mergeRows(sheet, groupStart, groupEnd, COL_IDX_PD_STATUS, COL_IDX_PD_STATUS);
                    mergeRows(sheet, groupStart, groupEnd, COL_IDX_PD_TOTAL_WORK, COL_IDX_PD_TOTAL_WORK);
                    mergeRows(sheet, groupStart, groupEnd, COL_IDX_PD_TOTAL_COST, COL_IDX_PD_TOTAL_COST);
                    mergeRows(sheet, groupStart, groupEnd, COL_IDX_PD_REVENUE, COL_IDX_PD_REVENUE);
                }

                HSSFRow regionRow = sheet.getRow(groupStart);
                HSSFCell regionCell = regionRow.getCell(COL_IDX_PRODUCT);
                regionCell.setCellStyle(productStyle);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void mergeRows(HSSFSheet sheet, int firstRow, int lastRow, int firstCol, int lastCol) {
        CellRangeAddress regionProduct = new CellRangeAddress(firstRow, lastRow, firstCol, lastCol);
        sheet.addMergedRegion(regionProduct);
    }
    private static void groupRows(HSSFSheet sheet, int firstRow, int lastRow) {
        sheet.groupRow(firstRow + 1, lastRow);
    }

    private static boolean _isCellBlank(HSSFCell cell) {
        String s = _getCellValue(cell);
        return s == null || s.trim().length() == 0;
    }

    private static String _getCellValue(HSSFCell cell) {
        if (cell==null) {
            return "";
        }
        String cellValue = null;

        FormulaEvaluator evaluator = cell.getSheet().getWorkbook().getCreationHelper().createFormulaEvaluator();

        CellType type = cell.getCellTypeEnum();

        if (CellType.STRING == type) {
            cellValue = cell.getStringCellValue().trim();
        } else if (CellType.NUMERIC == type) {
            cellValue = String.valueOf(cell.getNumericCellValue()).trim();
        } else if (CellType.BOOLEAN == type) {
            cellValue = String.valueOf(cell.getBooleanCellValue()).trim();
        } else if (CellType.FORMULA == type) {
            cellValue = evaluator.evaluateInCell(cell).toString().trim();
        } else {
            cellValue = "";
        }

        return cellValue;
    }


} public class HtmlGenerator {
//    public static final String HTML_FILE = PropertiesCache.OUTPUT_FOLDER + "/JiraScrumCards-{boardAndSprintName}-{date}.html";
    public static final String HTML_FILE = FoldersConsts.OUTPUT_FOLDER + "/JiraScrumCards-{boardName}-{sprintName}.html";
    public static final String CSS_CARD_ID_LIGHT = "cardview_light";
    public static final String CSS_CARD_ID_DARK = "cardview_dark";

    public static final String TAG_LEFT = "<";
    public static final String TAG_LEFT_ESCAPE = "&lt;";
    public static final String TAG_RIGHT = ">";
    public static final String TAG_RIGHT_ESCAPE = "&gt;";
    public static final String TAG_QUOT = "\"";
    public static final String TAG_QUOT_ESCAPE = "&quot;";

    private static final String PX_TITLE_HEIGHT = "130px";
    private static final String PX_ONEROW_HEIGHT = "110px";
    private static final String PX_THREEROWS_HEIGHT = "280px";

    private static final String CIRCLE_OPACITY    = "0.8";  //小圆点的透明度，以防完全挡住背后的文字

    private static final int A4_CARD_COLS = 2; //A4纸多少列

    private static final int A4_CARD_ROWS = 3; //A4纸多少行
    private static final String TEMPLATE_CARD_TITLE   = "<p><u><div class=\"kanban_name\">{boardName}</div></u></p>" +
                                                     "<div class=\"parent_name\">[{parentType}][{parentKey}]: {parentName}</div>";
    private static final String TEMPLATE_CARD_CONTENT = "<div class=\"issue_name\">[{issueType}][{issueKey}]: {issueName}</div>";
    private static final String TEMPLATE_OWNER        = "<div class=\"owner_name\">{ownerName}</div>";
    private static final String TEMPLATE_CIRCLE        =
            "<div style=\"border-style:solid; border-color:black; border-width:1px; border-radius:50%; display: inline-block; " +
            "   position:absolute;z-index:100;float:{float};top:{circletop}; left:{circleleft}; " +
            "   background: {bgcolor}; opacity:{opacity}; height: {dimension}; width: {dimension}; \">  \n" +
            "   <span style=\"display: block; color:{fontcolor}; font-size:{fontsize}; text-align: center; " +
            "    height:{dimension}; line-height: {dimension};\"> " +
            "       {content} " +
            "   </span>" +
            "</div>";

    private static final String TEMPLATE_CIRCLE_PRIORITY = TEMPLATE_CIRCLE
            .replaceAll("\\{float\\}",      "left")
            .replaceAll("\\{dimension\\}",  "80px")
            .replaceAll("\\{opacity\\}",    "0.8")
            .replaceAll("\\{circletop\\}",  "10px")
            .replaceAll("\\{circleleft}",   "10px");

    private static final String TEMPLATE_CIRCLE_BLOOD = TEMPLATE_CIRCLE
            .replaceAll("\\{float\\}",      "right")
            .replaceAll("\\{dimension\\}",  "40px")
            .replaceAll("\\{opacity\\}",    "0.8")
            .replaceAll("\\{circletop\\}",  "10px")
            .replaceAll("\\{circleleft}",   "445px");

    //Key = P1, Value = {BackgroundColor, FontColor}
    private static final Map<String, String[]> PRIORITY_COLORS = new HashMap<>();

    public static String generate(String boardName, String sprintName, Set<JiraIssue> issueSet) {
        boardName = boardName.replaceAll(" ", "");
        sprintName = sprintName.replaceAll(" ", "");

        String htmlPath = HTML_FILE
                .replace("{boardName}", boardName)
                .replace("{sprintName}", sprintName)
                .replace("{date}", String.valueOf(System.currentTimeMillis()));

        File htmlFile = new File(htmlPath);
        if (htmlFile.exists()) {
            htmlFile.delete();
        }

        String htmlCode = renderHTML(issueSet, boardName, sprintName);
        PrintStream printStream = null;
        try{
            //打开文件
            printStream = new PrintStream(new FileOutputStream(htmlFile));
        }catch(FileNotFoundException e){
            e.printStackTrace();
        }

        //将HTML文件内容写入文件中
        printStream.println(htmlCode);
        System.out.println("生成成功! 路径：" + htmlPath);
        return htmlPath;
    }

    private static String renderHTML(Set<JiraIssue> issueIdSet, String boardName, String sprintName) {
        ContainerTag pageTable = table().withStyle("width:92%; border-spacing:20px");

        int i = 1;

        ContainerTag pageBreak = null;
        ContainerTag pageNewLine = null;
        for (JiraIssue jiraIssue : issueIdSet) {
            //超过一页的卡片数量就是用TBody分页
            if (i % (A4_CARD_COLS * A4_CARD_ROWS) == 1) {
                pageBreak = tbody().withClass("sheet");
                pageTable.with(pageBreak);
            }

            //启动新的TR
            if (i % A4_CARD_COLS == 1) {
                pageNewLine = tr();
                pageBreak.with(pageNewLine);
//                pageTable.with(pageNewLine);
            }

            pageNewLine.with(
                td().withStyle("position:relative").with(renderSingleCard(jiraIssue, boardName, sprintName))
            );

            i++;
        }

        return
        html(
            head(
                title("Jira看板打印 - v2.0"),
                meta().attr("http-equiv", "Content-Type").attr("content", "text/html;charset=utf-8"),
                style().withText(HtmlCss.CARD_COMMON).withText(HtmlCss.PAPER)
                        .withText(HtmlCss.CARD_LIGHT).withText(HtmlCss.CARD_DARK)
//                link().withRel("stylesheet").withHref("/css/main.css")
            ),
            body(pageTable).withClass("A4")
        ).renderFormatted()
                .replaceAll(TAG_LEFT_ESCAPE, TAG_LEFT)
                .replaceAll(TAG_RIGHT_ESCAPE, TAG_RIGHT)
                .replaceAll(TAG_QUOT_ESCAPE, TAG_QUOT)
                ;
        //EscapeUtil.class Text.class When rendering it will escape some tags.
    }

    private static ContainerTag renderSingleCard(JiraIssue jiraIssue, String boardName, String sprintName) {
        String card_bgcolor = PropertiesCache.getProp(PropertiesCache.P_COLOR_STORY_BG, JiraColor.STORY.getHex()); //Story背景色

        if (jiraIssue.isTask()){
            card_bgcolor = PropertiesCache.getProp(PropertiesCache.P_COLOR_TASK_BG, JiraColor.TASK.getHex());
        }

//        JiraBoard jiraBoard = JIRA.getBoardCache(jiraIssue.getBoardId());
//        String boardName = (jiraBoard == null ? "" : jiraBoard.getBoardName());

        //增加优先级的圆形，图层向左上角浮动

        //增加血缘关系序号展示位，图层向右上角浮动，用于关联故事与子任务的关系
        String bloodBgColor = "#e14fa6";
        String bloodFontColor = "#FFF";
        String bloodContent = "";

        //生成卡片颜色：背景色 + 血缘小圆点色
        String[] cardColors;
        if (jiraIssue.isParent()) {
            bloodContent = String.valueOf(JSCPUtils.getNumAtLast(jiraIssue.getIssueKey()));
            cardColors = JiraColor.getCardColor(jiraIssue, jiraIssue.getOwner(), bloodContent);
        } else {
            bloodContent = String.valueOf(JSCPUtils.getNumAtLast(jiraIssue.getParentKey()));
            cardColors = JiraColor.getCardColor(jiraIssue, jiraIssue.getOwner(), bloodContent);
        }

        bloodBgColor = cardColors[1];
        bloodFontColor = JiraColor.isColorDark(bloodBgColor) ? "#FFF" : "#000";
        if (jiraIssue.isSubTask()) {
            card_bgcolor = cardColors[0];
        }
        String cardIdCss = JiraColor.isColorDark(card_bgcolor) ? CSS_CARD_ID_DARK : CSS_CARD_ID_LIGHT;

        //bloodFontSize，防止数字溢出小圆点,
        //20190818 这都考虑到了，我好牛逼啊
        int bloodFontSize = 18;
        if (bloodContent.length() == 4) {
            bloodFontSize = 15;
        } else if (bloodContent.length() > 4) {
            bloodFontSize = 11;
        } else if (bloodContent.length() > 6) {
            bloodFontSize = 9;
        }

//        ContainerTag bloodTD = td().attr("style", "border-top:0px none #000;font-size:" + bloodFontSize + "px;")
//                .attr("bgcolor", "#FFF")
//                .attr("rowspan", "4")
//                .attr("valign", "top")
//                .withText(
//                    TEMPLATE_CIRCLE_BLOOD
//                        .replaceAll("\\{bgcolor\\}", bloodBgColor)
//                        .replaceAll("\\{fontcolor\\}", bloodFontColor)
//                        .replaceAll("\\{content\\}", bloodContent));

        String bloodDiv = TEMPLATE_CIRCLE_BLOOD
                .replaceAll("\\{bgcolor\\}",    bloodBgColor)
                .replaceAll("\\{fontcolor\\}",  bloodFontColor)
                .replaceAll("\\{fontsize\\}",   String.valueOf(bloodFontSize))
                .replaceAll("\\{content\\}",    bloodContent);
        //BLOOD展示位构建完毕................................................


        String issuePriority = jiraIssue.getPriority();
        String priorityDiv = "";
        if (StringUtils.isNotBlank(issuePriority)) {
            priorityDiv = TEMPLATE_CIRCLE_PRIORITY
                    .replaceAll("\\{bgcolor\\}", getPriorityColor(issuePriority, true))
                    .replaceAll("\\{fontcolor\\}", getPriorityColor(issuePriority, false))
                    .replaceAll("\\{fontsize\\}", "20")
                    .replaceAll("\\{content\\}", issuePriority);
        }


        //标题的缩略长度，需要减去类型和key的长度，尽量精确吧。
        int lenOfparentNameAbbr = Integer.valueOf(PropertiesCache.getProp(PropertiesCache.P_CARD_TITLE_ABBR_LEN))
                - jiraIssue.getParentType().length() - jiraIssue.getParentKey().length();

        ContainerTag cardTable = table().withId(cardIdCss)
                .attr("bgcolor", card_bgcolor)
                .with(
                 thead(
                        tr(
                                th().attr("scope","col").attr("width", "70%"),
                                th().attr("scope","col").attr("width", "30%")
                        )
                ),
                tr(
                        td().attr("colspan","2")
                            .attr("height", PX_TITLE_HEIGHT)
                            .attr("align", "center")
                            .attr("style", "border-top:0px none #000;")
                            .with(b(TEMPLATE_CARD_TITLE
                                    .replace("{boardName}",     boardName)
                                    .replace("{parentType}",    jiraIssue.getParentType())
                                    .replace("{parentKey}",     jiraIssue.getParentKey())
                                    .replace("{parentName}",    StringUtils.abbreviate(jiraIssue.getParentName(), lenOfparentNameAbbr))))
//                                    .replace("{parentName}",    StringUtils.abbreviate(lenStr4Test, lenOfparentNameAbbr))))
                )
        );

        if (jiraIssue.isParent()) {

            cardTable.with(
                tr(
                        td().attr("rowspan","3")
                            .attr("colspan", "2")
                            .attr("height", PX_THREEROWS_HEIGHT)
                            .attr("align", "left")
                            .attr("valign", "middle")
                            .withText(StringUtils.abbreviate(
                                    TEMPLATE_CARD_CONTENT
                                    .replace("{issueType}",     jiraIssue.getIssueType())
                                    .replace("{issueKey}",      jiraIssue.getIssueKey())
                                    .replace("{issueName}",     jiraIssue.getIssueName()),
//                                    .replace("{issueName}",     lenStr4Test),
                                    Integer.valueOf(PropertiesCache.getProp(PropertiesCache.P_CARD_STORY_ABBR_LEN))))
                )
            );
        } else {
            cardTable.with(
                tr(
                        td().attr("rowspan","3")
                            .attr("colspan", "1")
                            .attr("height", PX_THREEROWS_HEIGHT)
                            .attr("align", "left")
                            .attr("valign", "middle")
                            .withText(StringUtils.abbreviate(TEMPLATE_CARD_CONTENT
                                    .replace("{issueType}",     jiraIssue.getIssueType())
                                    .replace("{issueKey}",      jiraIssue.getIssueKey())
                                    .replace("{issueName}",     jiraIssue.getIssueName()),
//                                    .replace("{issueName}",     lenStr4Test),
                                    Integer.valueOf(PropertiesCache.getProp(PropertiesCache.P_CARD_ISSUE_ABBR_LEN)))),
//
                        td(TEMPLATE_OWNER.replace("{ownerName}",jiraIssue.getOwner()))
                            .attr("height", PX_ONEROW_HEIGHT)
                            .attr("width", "100px")
                ),
                tr(
                        td("估算:" + jiraIssue.getEstimate()).attr("rowspan", "2").attr("valign", "top")
                )
            );
        }

        return div()
                //添加优先级的浮层在左上角，血缘ID的浮层在右上角
                .withText(priorityDiv)
                .withText(bloodDiv)
                .with(cardTable);
    }

    //获取优先级的颜色
    private static String getPriorityColor(String priority, boolean isBackground) {
        //初始化获取配置属性
        if (PRIORITY_COLORS == null || PRIORITY_COLORS.isEmpty()) {
            String priorityKeys = PropertiesCache.getProp(PropertiesCache.P_COLOR_PRIORITY_KEY);
            String priorityBgColors = PropertiesCache.getProp(PropertiesCache.P_COLOR_PRIORITY_BG);
            String priorityFontColors = PropertiesCache.getProp(PropertiesCache.P_COLOR_PRIORITY_FONT);

            String[] priorityKeyArr = StringUtils.split(priorityKeys, ",");
            String[] priorityBgColorArr = StringUtils.split(priorityBgColors, ",");
            String[] priorityFontColorArr = StringUtils.split(priorityFontColors, ",");

            if (priorityKeyArr.length != priorityBgColorArr.length || priorityKeyArr.length != priorityFontColorArr.length) {
                return isBackground ? "#FFF" : "000";
            }

            for (int i = 0; i < priorityKeyArr.length; i++) {
                PRIORITY_COLORS.put(StringUtils.trim(priorityKeyArr[i]), new String[]{priorityBgColorArr[i], priorityFontColorArr[i]});
            }
        }

        String[] colors = PRIORITY_COLORS.get(priority);
        if (colors!=null) {
            return isBackground ? colors[0] : colors[1];
        }

        return isBackground ? "#FFF" : "000";
    }

    private static String lenStr4Test="US5-3_【AAAA】用户在么么钱包申请现金贷获得放款后，可通过支付宝、银行转账等线下还款方式进行还款，这样用户可以线下完成主动还款【海尔云贷现金贷项目】用户在么么钱包申请现金贷获得放款后，可通过支付宝、银行转账等线下还款方式进行还款，这样用户可以线下完成主动还款";
    public static void main(String[] args) {
//        generate("ETS", "0915", null);

        System.out.println(TEMPLATE_CIRCLE_PRIORITY);
    }
} public class VPLViaHtml extends ViewOfProductLine {
    public static final String HTML_FILE = FoldersConsts.OUTPUT_FOLDER + "/OQPL-{date}.html";

    public static String generate(Map<String, Map<String, JiraIssue>> epicIssuesMap) {
        if (epicIssuesMap == null || epicIssuesMap.size() == 0)  return null;

        //解析数据，输入易于HTML排版的数据二维信息
        sortIssues(epicIssuesMap);

        //html渲染
        String htmlPath = HTML_FILE.replace("{date}", DATE_FORMAT_SLIM.format(new Date()));

        File htmlFile = new File(htmlPath);
        if (htmlFile.exists()) {
            htmlFile.delete();
        }

        String htmlCode = renderHTML();
        PrintStream printStream = null;
        try{
            //打开文件
            printStream = new PrintStream(new FileOutputStream(htmlFile));
        }catch(FileNotFoundException e){
            e.printStackTrace();
        }

        //将HTML文件内容写入文件中
        printStream.println(htmlCode);
        System.out.println("本地生成HTML成功! 路径：" + htmlPath);

        String webUrl = "";
        try {
            webUrl = Publisher.publish(htmlPath);
        } catch (Exception e) {
            System.out.println("发布到Web服务器失败!");
            e.printStackTrace();
            //远程发布失败，打开本地
            return htmlPath;
        }

        return webUrl;
    }
    private static String renderHTML() {
        int totalWidth = 0;
        for (int pixel : COL_WIDTH_PIXEL) {
            totalWidth += pixel;
        }

        ContainerTag productTBody = tbody();
        ContainerTag html = html(
                head(
                        title(TITLE),
                        meta().attr("http-equiv", "Content-Type").attr("content", "text/html;charset=utf-8"),
                        style().withText(HtmlCss.TABLE_COMMON)
//                link().withRel("stylesheet").withHref("/css/main.css")
                ),
                body(div().withId("divCenterWithMargin")
                        .with(
                            h1(TITLE).attr("align","center"),
                            h4(DATE_FORMAT.format(new Date())).attr("align","right"),
                            table().withId("defaultTable").with(productTBody)
                        )
                )

        );

        int colIdx = 0;
        for (String headerName : COL_NAME_ARR) {
            productTBody.with(th(headerName).attr("width", (int)(COL_WIDTH_PIXEL[colIdx++] * 100 / totalWidth)+"%"));
        }

        String previousEpicGroupKey = "";
        String previousProjectGroupKey = "";

        String currEpicGroupKey = "";
        String currProjectGroupKey = "";

        ContainerTag newLine = null;
        for (String combinedKey : SORTED_ISSUES_MAP.keySet()) {
            JiraIssue jiraIssue = SORTED_ISSUES_MAP.get(combinedKey);
            JiraEpic jiraEpic   = jiraIssue.getJiraEpic();

            newLine = tr();

            int epicGroupCount    = 0;
            int projectGroupCount = 0;

            currEpicGroupKey = getEpicGroupKey(combinedKey);
            currProjectGroupKey = getEpicProjectGroupKey(combinedKey);

            //新的Epic
            if (!currEpicGroupKey.equals(previousEpicGroupKey)) {
                previousEpicGroupKey = currEpicGroupKey;
                epicGroupCount = getEpicGroupCount(combinedKey);
            }
            //新的Project
            if (!getEpicProjectGroupKey(combinedKey).equals(previousProjectGroupKey)) {
                previousProjectGroupKey = currProjectGroupKey;
                projectGroupCount = getProjectGroupCount(combinedKey);
            }

            if (epicGroupCount >= 1) {
                String epicUrl = JIRA.serverUrl + JIRA.BROWSE_ISSUE_URL.replace("{issueKey}", jiraIssue.getJiraEpic().getEpicKey());
                newLine.with(td()
                                .attr("rowspan", epicGroupCount)
                                .with(a(jiraIssue.getJiraEpic().getEpicName()).withTarget("_blank").withHref(epicUrl)));
                newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).attr("rowspan", epicGroupCount).withText(jiraEpic.getEpicOwner()));
                newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).attr("rowspan", epicGroupCount).withText(jiraEpic.getCustomPriority()));
                newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).attr("rowspan", epicGroupCount).withText(jiraEpic.getEpicStatus()));
                newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).attr("rowspan", epicGroupCount).withText(jiraEpic.getCustomTotalWorking()));
                newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).attr("rowspan", epicGroupCount).withText(jiraEpic.getCost()));
                newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).attr("rowspan", epicGroupCount).withText(jiraEpic.getCustomRevenueInfo()));
            }
            if (projectGroupCount >= 1) {
                String[] displayProject = getDisplayProject(jiraEpic, jiraIssue);
                String projectKey  = displayProject[0];
                String projectName =  displayProject[1];

                String backlogUrl = JIRA.serverUrl
                        + JIRA.BROWSE_ISSUE_URL.replace("{issueKey}", projectKey)
                        + "?view=planning";

                newLine.with(td()
                                .attr("rowspan", projectGroupCount)
                                .with(a(projectName).withTarget("_blank").withHref(backlogUrl)));
            }
            if (StringUtils.isNotBlank(jiraIssue.getIssueKey())) {
                String issueUrl = JIRA.serverUrl
                        + JIRA.BROWSE_ISSUE_URL.replace("{issueKey}", jiraIssue.getIssueKey());
                newLine.with(td()
                        .with(a(jiraIssue.getIssueName()).withTarget("_blank").withHref(issueUrl)));
            } else {
                newLine.with(td().withText(jiraIssue.getIssueName()));
            }
            newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).withText(jiraIssue.getOwner()));
            newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).withText(jiraIssue.getPriority()));
            newLine.with(td().withStyle(HtmlCss.CELL_TEXT_CENTER).withText(jiraIssue.getIssueStatus()));
            newLine.with(td().withText(jiraIssue.getDescription()));
            newLine.with(td().withText(jiraIssue.getCustomEstimateDone()));
            newLine.with(td().withText(jiraIssue.getCustomActualDone()));
            newLine.with(td().withText(jiraIssue.getUpdatedAt()));

            productTBody.with(newLine);
        }

        return html.renderFormatted()
                .replaceAll(TAG_LEFT_ESCAPE, TAG_LEFT)
                .replaceAll(TAG_RIGHT_ESCAPE, TAG_RIGHT)
                .replaceAll(TAG_QUOT_ESCAPE, TAG_QUOT)
                ;
        //EscapeUtil.class Text.class When rendering it will escape some tags.
    }

} public class PropertiesCache {
//    public static final String OUTPUT_FOLDER = System.getProperty("user.dir") + File.separator + "JSCP_FILES" ;
//    public static final String CONFIG_FILE = System.getProperty("user.dir") + File.separator +"CONF" + File.separator + "jscp.properties";

    public static final String P_UPDATE_URL         = "autoupdate.url";
    public static final String P_BOARD_FILTER       = "boardfilter";
    public static final String P_HOST               = "host";
    public static final String P_USER               = "user";
    public static final String P_PWD                = "pwd";
    public static final String P_COLORFUL           = "colorful";
    public static final String P_SHOWALLCOLS        = "excelShowAllColumns";
    public static final String P_CUSTOM_FIELD_KEYS  = "excel.customfield.keys";
    public static final String P_CUSTOM_FIELD_NAMES = "excel.customfield.names";
    public static final String P_JIRA_CUSTOM_FIELD_KEYS  = "jira.customfield.keys";
    public static final String P_JIRA_CUSTOM_FIELD_NAMES = "jira.customfield.names";


    public static final String P_ISSUE_STATUS_CATEGORY_3KEYS = "issue.status.category.3keys";


    public static final String P_SMB_DOMAIN = "smb.domain";
    public static final String P_SMB_USER = "smb.login.user";
    public static final String P_SMB_PASSWORD = "smb.login.password";
    public static final String P_SMB_LOGIN_DOMAIN = "smb.login.domain";
    public static final String P_SMB_SHARE_ROOT = "smb.share.root";
    public static final String P_SMB_SHARE_PATH = "smb.share.path";
    public static final String P_WEBSERVER_URL = "webserver.url";


    public static final String ISSUE_CUSTOMFIELD_PREFIX = "issue.customfield.";
    public static final String P_ISSUE_CUSTOM_PRIORITY     = ISSUE_CUSTOMFIELD_PREFIX + "priority";
    public static final String P_ISSUE_CUSTOM_ESTIMATEDONE = ISSUE_CUSTOMFIELD_PREFIX + "estimatedone";
    public static final String P_ISSUE_CUSTOM_ACTUAL_DONE  = ISSUE_CUSTOMFIELD_PREFIX + "actualdone";
    public static final String P_ISSUE_CUSTOM_REVENUEINFO  = ISSUE_CUSTOMFIELD_PREFIX + "revenueinfo";
    public static final String P_ISSUE_CUSTOM_TOTALWORKING = ISSUE_CUSTOMFIELD_PREFIX + "totalworking";
    public static final String P_WORKING_TO_COST = "working.to.cost.rate";

    public static final String P_CARD_TITLE_FONT_SIZE   = "card.title.font.size";
    public static final String P_CARD_ISSUE_FONT_SIZE   = "card.issue.font.size";
    public static final String P_CARD_TITLE_ABBR_LEN    = "card.title.abbr.length";
    public static final String P_CARD_STORY_ABBR_LEN    = "card.story.abbr.length";
    public static final String P_CARD_ISSUE_ABBR_LEN    = "card.issue.abbr.length";

    public static final String P_COLOR_STORY_BG         = "card.color.story.bg";
    public static final String P_COLOR_TASK_BG          = "card.color.task.bg";
    public static final String P_COLOR_SUBTASK_BG       = "card.color.subtask.bg";
    public static final String P_COLOR_PRIORITY_KEY     = "card.color.priority.key";    //P0; P1; P2; P3
    public static final String P_COLOR_PRIORITY_BG      = "card.color.priority.bg";     //#FFF; #000; #FF0400;
    public static final String P_COLOR_PRIORITY_FONT    = "card.color.priority.font";   //#000; #000; #000

    public static final String P_ISSUE_PRIORITY_KEY_TO_NUM = "issue.priority.key.to.num"; //Issue优先级映射为数字

    private static final String KEY = System.getProperty("java.version");

    private static final Properties configProp = new OrderedProperties();

    private static final Map<String, String> modifiedMap = new LinkedHashMap<>();

    private PropertiesCache() {
    }

    static {
        configProp.put("JSCP_OWNER","nicholas.qu");

        configProp.put("#JIRA_LOGIN","#");
        configProp.put(P_BOARD_FILTER,"");
        configProp.put(P_HOST,"");
        configProp.put(P_USER,"");
        configProp.put(P_PWD,"");

        configProp.put("#JIRA_New_Feature","#");
        //New Features
        configProp.put(P_COLORFUL, "1");
        configProp.put(P_SHOWALLCOLS, "0");
        configProp.put(P_CUSTOM_FIELD_KEYS, "fields>customfield_11001");
        configProp.put(P_CUSTOM_FIELD_NAMES, "实际耗费时间");
        configProp.put(P_JIRA_CUSTOM_FIELD_KEYS, "customfield_11400");
        configProp.put(P_JIRA_CUSTOM_FIELD_NAMES, "epicPriority");
        configProp.put(P_UPDATE_URL, "");

        configProp.put("#SMB","#");

        configProp.put(P_SMB_DOMAIN, "");
        configProp.put(P_SMB_USER, "");
        configProp.put(P_SMB_PASSWORD, "");
        configProp.put(P_SMB_LOGIN_DOMAIN, "");
        configProp.put(P_SMB_SHARE_ROOT, "");
        configProp.put(P_SMB_SHARE_PATH, "");
        configProp.put(P_WEBSERVER_URL, "");

        configProp.put("#CUSTOM_FIELDS","#");

        configProp.put(P_ISSUE_STATUS_CATEGORY_3KEYS, "new,indeterminate,done");

        configProp.put(P_ISSUE_CUSTOM_PRIORITY, "fields>customfield_11500");
        configProp.put(P_ISSUE_CUSTOM_ESTIMATEDONE, "fields>customfield_11603");
        configProp.put(P_ISSUE_CUSTOM_ACTUAL_DONE, "fields>customfield_11602");
        configProp.put(P_ISSUE_CUSTOM_REVENUEINFO, "fields>customfield_11600");
        configProp.put(P_ISSUE_CUSTOM_TOTALWORKING, "fields>customfield_11501");
        configProp.put(P_WORKING_TO_COST, "0.16091954");

        configProp.put("#FONT_SIZE","#");
        configProp.put(P_CARD_TITLE_FONT_SIZE, "16");
        configProp.put(P_CARD_ISSUE_FONT_SIZE, "24");
        configProp.put(P_CARD_TITLE_ABBR_LEN, "85");
        configProp.put(P_CARD_STORY_ABBR_LEN, "160");
        configProp.put(P_CARD_ISSUE_ABBR_LEN, "140");

        configProp.put("#COLORS","#");
        configProp.put(P_COLOR_STORY_BG, "#FFFF00");
        configProp.put(P_COLOR_TASK_BG,  "#808000");
        configProp.put(P_COLOR_PRIORITY_KEY,  "P0,P1,P2,P3");
        configProp.put(P_COLOR_PRIORITY_BG,   "#DC143C,#8A2BE2,#f79232,#707070");
        configProp.put(P_COLOR_PRIORITY_FONT, "#FFFFFF,#FFFFFF,#000000,#000000");

        configProp.put("#DICTIONARY","#");
        //优先级映射的数字最大2位，不要超过90
        configProp.put(P_ISSUE_PRIORITY_KEY_TO_NUM, "{\"P0\":10,\"P1\":30,\"P2\":50,\"P3\":70}");

        configProp.put("#OTHERS","#");

        read();
    }

    private static Properties read() {
        File configFile = new File(FoldersConsts.CONFIG_FILE);

        if (configFile == null) {
            return configProp;
        }

        if (!configFile.exists()) {
            try {
                configFile.getParentFile().mkdirs();
                configFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        FileReader reader = null;
        try {
            reader = new FileReader(configFile);
            int hashCode1 = configProp.hashCode();
            Properties properties = new OrderedProperties();
            properties.load(reader);
            int hashCode2 = properties.hashCode();
            if (hashCode1 != hashCode2) {
                modifiedMap.clear();
                configProp.putAll(properties);
                flush();
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        notifyUpdates();

        return configProp;
    }

    public static void flush() {
        if (modifiedMap.size() == 0) {
            return;
        }

        System.out.println("FLUSH CACHE.");
        File configFile = new File(FoldersConsts.CONFIG_FILE);

        if (!configFile.exists()) {
            System.out.println("生成缓存文件..." + configFile.getAbsolutePath());
        }

        FileWriter writer = null;
        try {
            writer = new FileWriter(configFile);
            configProp.putAll(modifiedMap);
            configProp.store(writer, "Jira Card Printer Cache");
            modifiedMap.clear();

            notifyUpdates();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void setHost(String value) {
        modifiedMap.put(P_HOST, value);
    }

    public static void setUserName(String value) {
        modifiedMap.put(P_USER, value);
    }

    public static void setPassword(String value) {
        modifiedMap.put(P_PWD, JSCPUtils.encrypt(value, KEY));
    }

    public static void setBoardFilter(String value) {
        modifiedMap.put(P_BOARD_FILTER, value);
    }

    public static String getHost() {
        return (String)configProp.get(P_HOST);
    }

    public static String getUserName() {
        return (String)configProp.get(P_USER);
    }

    public static String getPassword() {
        String pwd = (String)configProp.get(P_PWD);
        return JSCPUtils.decrypt(pwd, KEY);
    }

    public static String getBoardFilter() {
        return (String)configProp.get(P_BOARD_FILTER);
    }

    public static String getProp(String key) {
        return getProp(key, "");
    }

    public static String getProp(String key, String ifNullValue) {
        String value = System.getenv(key);
        if (value == null) {
            value = System.getProperty(key);
        }
        if (value == null) {
            value = (String) configProp.get(key);
        }

        return value == null ? ifNullValue : value;
    }

    public static String getUpdateUrl() {
        return getProp(P_UPDATE_URL);
    }
    public static void setProp(String key, String value) {
        modifiedMap.put(key, value);
    }

    public static Map<String, String> getJiraCustomFields() {
        String customFieldKeyStr = (String)configProp.get(P_JIRA_CUSTOM_FIELD_KEYS);
        String customFieldNameStr = (String)configProp.get(P_JIRA_CUSTOM_FIELD_NAMES);

        if (StringUtils.isAnyBlank(customFieldKeyStr,customFieldNameStr)) {
            return new LinkedHashMap<>();
        }

        String[] customFieldKeyArr = StringUtils.split(customFieldKeyStr,",");
        String[] customFieldNameArr = StringUtils.split(customFieldNameStr,",");

        if (customFieldKeyArr.length != customFieldNameArr.length) {
            System.out.println("配置异常!" + P_JIRA_CUSTOM_FIELD_KEYS + "!=" + P_JIRA_CUSTOM_FIELD_NAMES);
            return new LinkedHashMap<>();
        }

        Map<String, String> jiraCustomFieldMap = new LinkedHashMap<String, String>();
        for (int i = 0; i<customFieldKeyArr.length; i++) {
            jiraCustomFieldMap.put(customFieldNameArr[i], customFieldKeyArr[i]);
        }

        return jiraCustomFieldMap;
    }

    public static void notifyUpdates() {
        //通知css更新
        HtmlCss.notifyCssUpdate();
    }

} @Data
@AllArgsConstructor
public class CheckBoxNodeData<T> implements Serializable {

	private boolean checked;

	private String item;

//	private String hiddenjson;

	public String toString() {
		return JSON.toJSONString(this);
	}

} public class CheckBoxNodeEditor extends AbstractCellEditor implements
        TreeCellEditor {

    private final CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();

    private final JTree theTree;

    public CheckBoxNodeEditor(final JTree tree) {
        theTree = tree;
    }

    @Override
    public Object getCellEditorValue() {
        final CheckBoxNodePanel panel = renderer.getPanel();

        final CheckBoxNodeData checkBoxNode =
                new CheckBoxNodeData(panel.checkbox.isSelected(), panel.item.getText());
        return checkBoxNode;
    }

    @Override
    public boolean isCellEditable(final EventObject event) {
        if (!(event instanceof MouseEvent)) return false;
        final MouseEvent mouseEvent = (MouseEvent) event;

        final TreePath path =
                theTree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
        if (path == null) return false;

        final Object node = path.getLastPathComponent();
        if (!(node instanceof DefaultMutableTreeNode)) return false;
        final DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) node;

        final Object userObject = treeNode.getUserObject();
        return userObject instanceof CheckBoxNodeData;
    }

    @Override
    public Component getTreeCellEditorComponent(final JTree tree,
                                                final Object value, final boolean selected, final boolean expanded,
                                                final boolean leaf, final int row) {

        final Component editor =
                renderer.getTreeCellRendererComponent(tree, value, true, expanded, leaf,
                        row, true);

        // editor always selected / focused
        final ItemListener itemListener = new ItemListener() {

            @Override
            public void itemStateChanged(final ItemEvent itemEvent) {
                if (stopCellEditing()) {
                    fireEditingStopped();
                }
            }
        };
        if (editor instanceof CheckBoxNodePanel) {
            final CheckBoxNodePanel panel = (CheckBoxNodePanel) editor;
            panel.checkbox.addItemListener(itemListener);
        }

        return editor;
    }
} public class CheckBoxNodeRenderer implements TreeCellRenderer {

    private final CheckBoxNodePanel panel = new CheckBoxNodePanel();

    private final DefaultTreeCellRenderer defaultRenderer =
            new DefaultTreeCellRenderer();

    private final Color selectionForeground, selectionBackground;
    private final Color textForeground, textBackground;

    protected CheckBoxNodePanel getPanel() {
        return panel;
    }

    public CheckBoxNodeRenderer() {
        final Font fontValue = UIManager.getFont("Tree.font");
        if (fontValue != null) panel.item
                .setFont(fontValue);

        final Boolean focusPainted = (Boolean) UIManager.get("Tree.drawsFocusBorderAroundIcon");
        panel.checkbox.setFocusPainted(focusPainted != null && focusPainted);

        selectionForeground = UIManager.getColor("Tree.selectionForeground");
        selectionBackground = UIManager.getColor("Tree.selectionBackground");
        textForeground = UIManager.getColor("Tree.textForeground");
        textBackground = UIManager.getColor("Tree.textBackground");
    }

    // -- TreeCellRenderer methods --

    @Override
    public Component getTreeCellRendererComponent(final JTree tree,
                                                  final Object value, final boolean selected, final boolean expanded,
                                                  final boolean leaf, final int row, final boolean hasFocus) {
        CheckBoxNodeData data = null;
        if (value instanceof DefaultMutableTreeNode) {
            final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
            final Object userObject = node.getUserObject();
            if (userObject instanceof CheckBoxNodeData) {
                data = (CheckBoxNodeData) userObject;
            }
        }

        if (data == null) {
            // not a check box node; return default cell renderer
            return defaultRenderer.getTreeCellRendererComponent(tree, value,
                    selected, expanded, leaf, row, hasFocus);
        }

        String displayName = data.getItem();
        JiraIssue jiraIssue = JIRA.getObjectFromDisplayName(displayName);

//        final String stringValue =
//                tree.convertValueToText(value, selected, expanded, leaf, row, false);

        panel.checkbox.setSelected(false);
        panel.item.setText(data.getItem());

        panel.setEnabled(tree.isEnabled());

        if (selected) {
            panel.setForeground(selectionForeground);
            panel.setBackground(selectionBackground);
            panel.item.setForeground(selectionForeground);
            panel.item.setBackground(selectionBackground);
        } else {
            panel.setForeground(textForeground);
            panel.setBackground(textBackground);
            panel.item.setForeground(textForeground);
            panel.item.setBackground(textBackground);
        }

        panel.checkbox.setSelected(data.isChecked());

        if (jiraIssue.isStory()) {
            panel.item.setIcon(JiraSwingConsts.IMG_STORY);
        } else if (jiraIssue.isEpic()) {
            panel.item.setIcon(JiraSwingConsts.IMG_EPIC);
        } else if (jiraIssue.isTask()) {
            panel.item.setIcon(JiraSwingConsts.IMG_TASK);
        } else if (jiraIssue.isSubTask()) {
            panel.item.setIcon(JiraSwingConsts.IMG_SUBTASK);
        }

        Font font = panel.item.getFont();
        Map attributes = font.getAttributes();
        attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
        Font doneFont = new Font(attributes);

        Map attributes2 = font.getAttributes();
        attributes2.put(TextAttribute.STRIKETHROUGH, false);
        Font usualFont  = new Font(attributes2);


        if (jiraIssue.isDone()) {
            panel.item.setFont(doneFont);
            panel.item.setForeground(Color.lightGray);
        } else if (jiraIssue.isDoing()) {
            panel.item.setFont(usualFont);
            panel.item.setForeground(new Color(78, 170, 222));
        } else {
            panel.item.setFont(usualFont);
            panel.item.setForeground(new Color(212, 159, 79));
        }

        return panel;
    }


} public class JiraListCellRender extends DefaultListCellRenderer {
    private ArrayList jiraList;

    public JiraListCellRender(ArrayList jiraList) {
        this.jiraList = jiraList;
    }
    
    public Component getListCellRendererComponent(JList list, Object value,
                                                  int index, boolean isSelected, boolean cellHasFocus) {
        super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

        if (index >= jiraList.size()) return this;

        Object object = jiraList.get(index);

        if (object instanceof JiraSprint) {
            JiraSprint jiraSprint = (JiraSprint) object;

            if (!jiraSprint.isActive()) {
                setForeground(Color.lightGray);

                Font font = getFont();
                Map attributes = font.getAttributes();
                attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
                setFont(new Font(attributes));
            }
        }

        return this;
    }
}
import cc.xiaoquer.jira.api.JIRA;
import cc.xiaoquer.jira.api.beans.JiraBoard;
import cc.xiaoquer.jira.api.beans.JiraEpic;
import cc.xiaoquer.jira.api.beans.JiraIssue;
import cc.xiaoquer.jira.api.beans.JiraSprint;
import cc.xiaoquer.jira.autoupdater.JiraAutoUpdater;
import cc.xiaoquer.jira.excel.ExcelProcessor4Sprint;
import cc.xiaoquer.jira.excel.VPLViaExcel;
import cc.xiaoquer.jira.html.HtmlGenerator;
import cc.xiaoquer.jira.html.VPLViaHtml;
import cc.xiaoquer.jira.storage.PropertiesCache;
import cc.xiaoquer.jira.uicomponents.checkboxtree.CheckBoxNodeData;
import cc.xiaoquer.jira.uicomponents.checkboxtree.CheckBoxNodeEditor;
import cc.xiaoquer.jira.uicomponents.checkboxtree.CheckBoxNodeRenderer;
import cc.xiaoquer.jira.uicomponents.jlist.JiraListCellRender;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.io.*;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;
    }

    private String getHHMMSS() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");

        return sdf.format(new Date());
    }

    private void renderBoards(String prefix) {
        resetResultSetAndText();
        //不强制刷新boards列表，只有点击登录才会强制刷新
        Map<String, JiraBoard> map = JIRA.getBoardMap(false);

        List<JiraBoard> sorted = new ArrayList<>();
        sorted.addAll(map.values());
        Collections.sort(sorted, new Comparator<JiraBoard>() {
            @Override
            public int compare(JiraBoard o1, JiraBoard o2) {
                return o1.getBoardName().compareTo(o2.getBoardName());
            }
        });

        for (JiraBoard jiraBoard : sorted) {

            if (prefix != null && prefix.length() > 0 && !jiraBoard.getBoardName().toLowerCase().contains(prefix.toLowerCase())) {
                continue;
            }

            boardsModel.addElement(combineDisplayName(jiraBoard.getBoardId(), jiraBoard.getBoardName()));
        }

//        for (int i=0;i<80;i++) {
//            boardsModel.addElement(i + "PMO-团队-大数据ETL_________" + i);
//        }

        scrollPaneBoard.setViewportView(jListBoards);
        scrollPaneBoard.updateUI();

        lblBoardCount.setText(STATUS_BOARD_COUNT.replace("{}", boardsModel.size() + ""));
        lblQueryStatus.setText(getHHMMSS() + "查询Boards成功");
    }

    private void resetResultSetAndText() {
        //清理子数据集
        boardsModel.clear();
        sprintsModel.clear();

        root.removeAllChildren();
        treeModel.reload();
        tree.setModel(null);

        lblBoardCount.setText(STATUS_BOARD_COUNT);
        lblQueryStatus.setText("");

        lblSelectedBoard.setText("");
        lblSelectedBoardSprint.setText("");
    }

    private void resetButton() {
        btnGenerate.setEnabled(false);
        btnExport.setEnabled(false);
        btnProductHtml.setEnabled(false);
        btnProductExcel.setEnabled(false);
    }

    private void renderSprints(String boardId) {
        resetButton();

        if (boardId == null || boardId.length() == 0){
            return;
        }

        //清理子数据集
        sprintsModel.clear();

        root.removeAllChildren();
        treeModel.reload();
        tree.setModel(null);

        Map<String, JiraSprint> map = JIRA.getSprintMap(boardId);
        for (JiraSprint jiraSprint : map.values()) {

//            if (prefix != null && prefix.length() > 0 && !jiraBoard.getBoardName().startsWith(prefix)) {
//                continue;
//            }

            sprintsModel.addElement(combineDisplayName(jiraSprint.getSprintId(), jiraSprint.getSprintName()));
        }
        //自定义render, 关闭的sprint放在尾端，并灰色显示
        jlistSprints.setCellRenderer(new JiraListCellRender(new ArrayList(map.values())));

        scrollPaneSprint.setViewportView(jlistSprints);
        scrollPaneSprint.updateUI();

        lblSelectedBoard.setText("看板:" + getNameByDisplayName((String)jListBoards.getSelectedValue()));
        lblSelectedBoardSprint.setText("");

        lblSprintCount.setText(STATUS_SPRINT_COUNT.replace("{}", sprintsModel.size() + ""));
        lblQueryStatus.setText(getHHMMSS() + "查询Sprints成功");

        btnExport.setEnabled(true);
        btnProductHtml.setEnabled(true);
        btnProductExcel.setEnabled(true);
    }

    private void renderIssueTree(String boardDisplayName, String sprintDisplayName) {
        String boardId      = getIdByDisplayName(boardDisplayName);
        String sprintId     = getIdByDisplayName(sprintDisplayName);

        if (boardId == null || boardId.length() == 0 || sprintId == null || sprintId.length() == 0){
            return;
        }

        root.removeAllChildren();
        treeModel.reload();

        tree.setModel(null);

        int issuesCnt = 0;

        Map<String, JiraIssue> issueMap = JIRA.getIssueMapWithSameOrder(boardId, sprintId);