package io.github.biezhi.wechat.utils;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.vdurmont.emoji.EmojiParser;
import io.github.biezhi.wechat.exception.WeChatException;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.lang.reflect.Type;
import java.net.FileNameMap;
import java.net.URLConnection;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 微信公共静态方法
 *
 * @author biezhi
 * @date 2018/1/19
 */
@Slf4j
public class WeChatUtils {

    private static final Gson GSON        = new Gson();
    private static final Gson PRETTY_GSON = new GsonBuilder().setPrettyPrinting().create();

    /**
     * 正则匹配
     *
     * @param reg
     * @param text
     * @return
     */
    public static String match(String reg, String text) {
        Pattern pattern = Pattern.compile(reg);
        Matcher m       = pattern.matcher(text);
        if (m.find()) {
            return m.group(1);
        }
        return null;
    }

    public static Matcher matcher(String reg, String text) {
        Pattern pattern = Pattern.compile(reg);
        return pattern.matcher(text);
    }

    /**
     * 格式化微信消息
     *

     * 处理emoji表情、HTML转义符
     *
     * @param msg
     * @return
     */
    public static String formatMsg(String msg) {
        msg = msg.replace("<", "<")
                .replace(">", ">")
                .replace("
", "\n");
        return emojiParse(msg);
    }

    /**
     * 获取文件 MimeType
     *
     * @param fileUrl
     * @return
     */
    public static String getMimeType(String fileUrl) {
        FileNameMap fileNameMap = URLConnection.getFileNameMap();
        String      type        = fileNameMap.getContentTypeFor(fileUrl);
        return type;
    }

    public static String emojiParse(String text) {
        Matcher       m         = matcher("", text);
        StringBuilder sb        = new StringBuilder();
        int           lastStart = 0;
        while (m.find()) {
            String str = m.group(1);
            if (str.length() == 6) {

            } else if (str.length() == 10) {

            } else {
                str = "&#x" + str + ";";
                String tmp = text.substring(lastStart, m.start());
                sb.append(tmp + str);
                lastStart = m.end();
            }
        }
        if (lastStart < text.length()) {
            sb.append(text.substring(lastStart));
        }
        if (sb.length() > 0) {
            return EmojiParser.parseToUnicode(sb.toString());
        }
        return text;
    }

    public static String toPrettyJson(Object bean) {
        return PRETTY_GSON.toJson(bean);
    }

    public static String toJson(Object bean) {
        return GSON.toJson(bean);
    }

    public static <T> T fromJson(String json, Class<T> from) {
        return GSON.fromJson(json, from);
    }

    public static <T> T fromJson(String json, Type from) {
        return GSON.fromJson(json, from);
    }

    public static <T> T fromJson(FileReader fileReader, Type from) {
        return GSON.fromJson(fileReader, from);
    }

    public static <T> T fromJson(String json, TypeToken<T> typeToken) {
        return GSON.fromJson(json, typeToken.getType());
    }

    public static File saveFile(InputStream inputStream, String dirPath, String id) {
        return saveFileByDay(inputStream, dirPath, id, false);
    }

    public static File saveFileByDay(InputStream inputStream, String dirPath, String id, boolean byDay) {
        OutputStream outputStream = null;
        try {
            if (byDay) {
                dirPath = dirPath + "/" + DateUtils.getDateString();
            }
            File dir = new File(dirPath);
            if (!dir.isDirectory()) {
                dir.mkdirs();
            }
            File path = new File(dir, id);
            if (path.exists()) {
                path.delete();
            }
            outputStream = new FileOutputStream(path);

            byte[] buffer = new byte[1024];
            int    len    = 0;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            outputStream.flush();
            return path;
        } catch (Exception e) {
            throw new WeChatException(e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
            }
        }
    }

    public static <T> void writeJson(String file, T data) {
        try {
            FileWriter writer = new FileWriter(file);
            GSON.toJson(data, writer);
            writer.close();
        } catch (Exception e) {
            log.warn("写入JSON到文件: {} 失败", file, e);
        }
    }

    public static int random(int min, int max) {
        Random random = new Random();
        return random.nextInt(max) % (max - min + 1) + min;
    }

}


package io.github.biezhi.wechat.utils;

import lombok.extern.slf4j.Slf4j;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 日期工具类
 *
 * @author biezhi
 * @date 2018/1/19
 */
@Slf4j
public class DateUtils {

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd");

    public static void sendSleep(){
        int rand = 2 + WeChatUtils.random(100, 3000);
        DateUtils.sleep(rand);
    }

    /**
     * 休眠，单位: 毫秒
     *
     * @param ms
     */
    public static void sleep(long ms) {
        try {
            TimeUnit.MILLISECONDS.sleep(ms);
        } catch (InterruptedException e) {
            log.error("", e);
        }
    }

    /**
     * 获取字符串日期
     *
     * @return
     */
    public static String getDateString() {
        return DATE_FORMAT.format(new Date());
    }

}


package io.github.biezhi.wechat.api;

import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import io.github.biezhi.wechat.WeChatBot;
import io.github.biezhi.wechat.api.client.BotClient;
import io.github.biezhi.wechat.api.constant.Constant;
import io.github.biezhi.wechat.api.constant.StateCode;
import io.github.biezhi.wechat.api.enums.AccountType;
import io.github.biezhi.wechat.api.enums.ApiURL;
import io.github.biezhi.wechat.api.enums.RetCode;
import io.github.biezhi.wechat.api.model.*;
import io.github.biezhi.wechat.api.request.BaseRequest;
import io.github.biezhi.wechat.api.request.FileRequest;
import io.github.biezhi.wechat.api.request.JsonRequest;
import io.github.biezhi.wechat.api.request.StringRequest;
import io.github.biezhi.wechat.api.response.*;
import io.github.biezhi.wechat.exception.WeChatException;
import io.github.biezhi.wechat.utils.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.RequestBody;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static io.github.biezhi.wechat.api.constant.Constant.*;

/**
 * 微信API实现
 *
 * @author biezhi
 * @date 2018/1/21
 */
@Slf4j
public class WeChatApiImpl implements WeChatApi {

    private static final Pattern UUID_PATTERN          = Pattern.compile("window.QRLogin.code = (\\d+); window.QRLogin.uuid = \"(\\S+?)\";");
    private static final Pattern CHECK_LOGIN_PATTERN   = Pattern.compile("window.code=(\\d+)");
    private static final Pattern PROCESS_LOGIN_PATTERN = Pattern.compile("window.redirect_uri=\"(\\S+)\";");
    private static final Pattern SYNC_CHECK_PATTERN    = Pattern.compile("window.synccheck=\\{retcode:\"(\\d+)\",selector:\"(\\d+)\"}");

    private String    uuid;
    private boolean   logging;
    private int       memberCount;
    private WeChatBot bot;
    private BotClient client;

    /**
     * 所有账号
     */
    @Getter
    private Map<String, Account> accountMap = new HashMap<>();

    /**
     * 特殊账号
     */
    @Getter
    private List<Account> specialUsersList = Collections.EMPTY_LIST;

    /**
     * 公众号、服务号
     */
    @Getter
    private List<Account> publicUsersList = Collections.EMPTY_LIST;

    /**
     * 好友列表
     */
    @Getter
    private List<Account> contactList = Collections.EMPTY_LIST;

    /**
     * 群组
     */
    @Getter
    private List<Account> groupList = Collections.EMPTY_LIST;

    /**
     * 群UserName列表
     */
    private Set<String> groupUserNames = new HashSet<>();

    public WeChatApiImpl(WeChatBot bot) {
        this.bot = bot;
        this.client = bot.client();
    }

    /**
     * 自动登录
     */
    private void autoLogin() {
        String file = bot.config().assetsDir() + "/login.json";
        try {
            HotReload hotReload = WeChatUtils.fromJson(new FileReader(file), HotReload.class);
            hotReload.reLogin(bot);
        } catch (FileNotFoundException e) {
            this.login(false);
        }
    }

    @Override
    public void login(boolean autoLogin) {
        if (bot.isRunning() || logging) {
            log.warn("微信已经登录");
            return;
        }
        if (autoLogin) {
            this.autoLogin();
        } else {
            this.logging = true;
            while (logging) {
                this.uuid = pushLogin();
                if (null == this.uuid) {
                    while (null == this.getUUID()) {
                        DateUtils.sleep(10);
                    }
                    log.info("开始下载二维码");
                    this.getQrImage(this.uuid, bot.config().showTerminal());
                    log.info("请使用手机扫描屏幕二维码");
                }
                Boolean isLoggedIn = false;
                while (null == isLoggedIn || !isLoggedIn) {
                    String status = this.checkLogin(this.uuid);
                    if (StateCode.SUCCESS.equals(status)) {
                        isLoggedIn = true;
                    } else if ("201".equals(status)) {
                        if (null != isLoggedIn) {
                            log.info("请在手机上确认登录");
                            isLoggedIn = null;
                        }
                    } else if ("408".equals(status)) {
                        break;
                    }
                    DateUtils.sleep(300);
                }
                if (null != isLoggedIn && isLoggedIn) {
                    break;
                }
                if (logging) {
                    log.info("登录超时，重新加载二维码");
                }
            }
        }

        this.webInit();
        this.statusNotify();
        this.loadContact(0);

        log.info("应有 {} 个联系人，读取到联系人 {} 个", this.memberCount, this.accountMap.size());
        System.out.println();

        log.info("共有 {} 个群 | {} 个直接联系人 | {} 个特殊账号 ｜ {} 公众号或服务号",
                this.groupUserNames.size(), this.contactList.size(),
                this.specialUsersList.size(), this.publicUsersList.size());

        // 加载群聊信息，群成员
        this.loadGroupList();

        log.info("[{}] 登录成功.", bot.session().getNickName());
        this.startRevive();
        this.logging = false;
    }

    /**
     * 获取UUID
     *
     * @return 返回uuid
     */
    private String getUUID() {
        log.info("获取二维码UUID");
        // 登录
        ApiResponse response = this.client.send(new StringRequest("https://login.weixin.qq.com/jslogin")
                .add("appid", "wx782c26e4c19acffb").add("fun", "new"));

        Matcher matcher = UUID_PATTERN.matcher(response.getRawBody());
        if (matcher.find() && StateCode.SUCCESS.equals(matcher.group(1))) {
            this.uuid = matcher.group(2);
        }
        return this.uuid;
    }

    /**
     * 读取二维码图片
     *
     * @param uuid         二维码uuid
     * @param terminalShow 是否在终端显示输出
     */
    private void getQrImage(String uuid, boolean terminalShow) {
        String uid    = null != uuid ? uuid : this.uuid;
        String imgDir = bot.config().assetsDir();

        FileResponse fileResponse = this.client.download(
                new FileRequest(String.format("%s/qrcode/%s", Constant.BASE_URL, uid)));

        InputStream inputStream = fileResponse.getInputStream();
        File        qrCode      = WeChatUtils.saveFile(inputStream, imgDir, "qrcode.png");
        DateUtils.sleep(200);
        try {
            QRCodeUtils.showQrCode(qrCode, terminalShow);
        } catch (Exception e) {
            this.getQrImage(uid, terminalShow);
        }
    }

    /**
     * 检查是否登录
     *
     * @param uuid 二维码uuid
     * @return 返回登录状态码
     */
    private String checkLogin(String uuid) {
        String uid  = null != uuid ? uuid : this.uuid;
        String url  = String.format("%s/cgi-bin/mmwebwx-bin/login", Constant.BASE_URL);
        Long   time = System.currentTimeMillis();

        ApiResponse response = this.client.send(new StringRequest(url)
                .add("loginicon", true).add("uuid", uid)
                .add("tip", "1").add("_", time)
                .add("r", (int) (-time / 1000) / 1579)
                .timeout(30));

        Matcher matcher = CHECK_LOGIN_PATTERN.matcher(response.getRawBody());
        if (matcher.find()) {
            if (StateCode.SUCCESS.equals(matcher.group(1))) {
                if (!this.processLoginSession(response.getRawBody())) {
                    return StateCode.FAIL;
                }
                return StateCode.SUCCESS;
            }
            return matcher.group(1);
        }
        return StateCode.FAIL;
    }

    /**
     * 处理登录session
     *
     * @param loginContent 登录text
     * @return 返回是否处理成功
     */
    private boolean processLoginSession(String loginContent) {
        LoginSession loginSession = bot.session();
        Matcher      matcher      = PROCESS_LOGIN_PATTERN.matcher(loginContent);
        if (matcher.find()) {
            loginSession.setUrl(matcher.group(1));
        }
        ApiResponse response = this.client.send(new StringRequest(loginSession.getUrl()).noRedirect());
        loginSession.setUrl(loginSession.getUrl().substring(0, loginSession.getUrl().lastIndexOf("/")));

        String body = response.getRawBody();

        List<String> fileUrl = new ArrayList<>();
        List<String> syncUrl = new ArrayList<>();
        for (int i = 0; i < FILE_URL.size(); i++) {
            fileUrl.add(String.format("https://%s/cgi-bin/mmwebwx-bin", FILE_URL.get(i)));
            syncUrl.add(String.format("https://%s/cgi-bin/mmwebwx-bin", WEB_PUSH_URL.get(i)));
        }
        boolean flag = false;
        for (int i = 0; i < FILE_URL.size(); i++) {
            String indexUrl = INDEX_URL.get(i);
            if (loginSession.getUrl().contains(indexUrl)) {
                loginSession.setFileUrl(fileUrl.get(i));
                loginSession.setSyncUrl(syncUrl.get(i));
                flag = true;
                break;
            }
        }
        if (!flag) {
            loginSession.setFileUrl(loginSession.getUrl());
            loginSession.setSyncUrl(loginSession.getUrl());
        }

        loginSession.setDeviceId("e" + String.valueOf(System.currentTimeMillis()));

        BaseRequest baseRequest = new BaseRequest();
        loginSession.setBaseRequest(baseRequest);

        loginSession.setSKey(WeChatUtils.match("<skey>(\\S+)</skey>", body));
        loginSession.setWxSid(WeChatUtils.match("<wxsid>(\\S+)</wxsid>", body));
        loginSession.setWxUin(WeChatUtils.match("<wxuin>(\\S+)</wxuin>", body));
        loginSession.setPassTicket(WeChatUtils.match("<pass_ticket>(\\S+)</pass_ticket>", body));

        baseRequest.setSkey(loginSession.getSKey());
        baseRequest.setSid(loginSession.getWxSid());
        baseRequest.setUin(loginSession.getWxUin());
        baseRequest.setDeviceID(loginSession.getDeviceId());

        return true;
    }

    /**
     * 推送登录
     *
     * @return 返回uuid
     */
    private String pushLogin() {
        String uin = this.client.cookie("wxUin");
        if (StringUtils.isEmpty(uin)) {
            return null;
        }
        String url = String.format("%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s",
                Constant.BASE_URL, uin);

        JsonResponse jsonResponse = this.client.send(new JsonRequest(url));
        return jsonResponse.getString("uuid");
    }

    /**
     * 开启状态通知
     */
    private void statusNotify() {
        log.info("开启状态通知");

        String url = String.format("%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s",
                bot.session().getUrl(), bot.session().getPassTicket());

        this.client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Code", 3)
                .add("FromUserName", bot.session().getUserName())
                .add("ToUserName", bot.session().getUserName())
                .add("ClientMsgId", System.currentTimeMillis() / 1000));
    }

    /**
     * web 初始化
     */
    private void webInit() {
        log.info("微信初始化...");
        int r = (int) (-System.currentTimeMillis() / 1000) / 1579;
        String url = String.format("%s/webwxinit?r=%d&pass_ticket=%s",
                bot.session().getUrl(), r, bot.session().getPassTicket());

        JsonResponse response = this.client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest()));

        WebInitResponse webInitResponse = response.parse(WebInitResponse.class);

        List<Account> contactList = webInitResponse.getContactList();
        this.syncRecentContact(contactList);

        Account account = webInitResponse.getAccount();
        SyncKey syncKey = webInitResponse.getSyncKey();

        bot.session().setInviteStartCount(webInitResponse.getInviteStartCount());
        bot.session().setAccount(account);
        bot.session().setUserName(account.getUserName());
        bot.session().setNickName(account.getNickName());
        bot.session().setSyncKey(syncKey);
    }

    /**
     * 开启一个县城接收监听
     */
    private void startRevive() {
        bot.setRunning(true);
        Thread thread = new Thread(new ChatLoop(bot));
        thread.setName("wechat-listener");
        thread.setDaemon(true);
        thread.start();
    }

    /**
     * 心跳检查
     *
     * @return SyncCheckRet
     */
    @Override
    public SyncCheckRet syncCheck() {
        String url = String.format("%s/synccheck", bot.session().getSyncOrUrl());
        try {
            ApiResponse response = this.client.send(new StringRequest(url)
                    .add("r", System.currentTimeMillis())
                    .add("skey", bot.session().getSKey())
                    .add("sid", bot.session().getWxSid())
                    .add("uin", bot.session().getWxUin())
                    .add("deviceid", bot.session().getDeviceId())
                    .add("synckey", bot.session().getSyncKeyStr())
                    .add("_", System.currentTimeMillis())
                    .timeout(30)
            );

            Matcher matcher = SYNC_CHECK_PATTERN.matcher(response.getRawBody());
            if (matcher.find()) {
                if (!"0".equals(matcher.group(1))) {
                    log.debug("Unexpected sync check result: {}", response.getRawBody());
                    return new SyncCheckRet(RetCode.parse(Integer.valueOf(matcher.group(1))), 0);
                }
                return new SyncCheckRet(RetCode.parse(Integer.valueOf(matcher.group(1))), Integer.valueOf(matcher.group(2)));
            }
            return new SyncCheckRet(RetCode.UNKNOWN, 0);
        } catch (Exception e) {
            if (e instanceof SocketTimeoutException) {
                log.warn("心跳检查超时");
                return syncCheck();
            }
            log.error("心跳检查出错", e);
            return new SyncCheckRet(RetCode.UNKNOWN, 0);
        }
    }

    /**
     * 获取最新消息
     *
     * @return WebSyncResponse
     */
    @Override
    public WebSyncResponse webSync() {
        String url = String.format("%s/webwxsync?sid=%s&sKey=%s&passTicket=%s",
                bot.session().getUrl(), bot.session().getWxSid(),
                bot.session().getSKey(), bot.session().getPassTicket());

        JsonResponse response = this.client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("SyncKey", bot.session().getSyncKey())
                .add("rr", ~(System.currentTimeMillis() / 1000)));

        WebSyncResponse webSyncResponse = response.parse(WebSyncResponse.class);
        if (!webSyncResponse.success()) {
            log.warn("获取消息失败");
            return webSyncResponse;
        }
        bot.session().setSyncKey(webSyncResponse.getSyncKey());
        return webSyncResponse;
    }

    /**
     * 退出登录
     */
    @Override
    public void logout() {
        if (bot.isRunning()) {
            String url = String.format("%s/webwxlogout", bot.session().getUrl());
            this.client.send(new StringRequest(url)
                    .add("redirect", 1)
                    .add("type", 1)
                    .add("sKey", bot.session().getSKey()));
            bot.setRunning(false);
        }
        this.logging = false;
        this.client.cookies().clear();
        String file = bot.config().assetsDir() + "/login.json";
        new File(file).delete();
    }

    /**
     * 加载联系人信息
     *
     * @param seq 默认为0，当一次无法加载完全则大于0
     */
    @Override
    public void loadContact(int seq) {
        log.info("开始获取联系人信息");
        while (true) {
            String url = String.format("%s/webwxgetcontact?r=%s&seq=%s&skey=%s",
                    bot.session().getUrl(), System.currentTimeMillis(),
                    seq, bot.session().getSKey());

            JsonResponse response = this.client.send(new JsonRequest(url).jsonBody());

            JsonObject jsonObject = response.toJsonObject();
            seq = jsonObject.get("Seq").getAsInt();

            this.memberCount += jsonObject.get("MemberCount").getAsInt();
            List<Account> memberList = WeChatUtils.fromJson(WeChatUtils.toJson(jsonObject.getAsJsonArray("MemberList")), new TypeToken<List<Account>>() {});

            for (Account account : memberList) {
                if (null == account.getUserName()) {
                    accountMap.put(account.getUserName(), account);
                }
            }
            // 查看seq是否为0，0表示好友列表已全部获取完毕，若大于0，则表示好友列表未获取完毕，当前的字节数（断点续传）
            if (seq == 0) {
                break;
            }
        }

        this.contactList = new ArrayList<>(this.getAccountByType(AccountType.TYPE_FRIEND));

        this.publicUsersList = new ArrayList<>(this.getAccountByType(AccountType.TYPE_MP));
        this.specialUsersList = new ArrayList<>(this.getAccountByType(AccountType.TYPE_SPECIAL));
        Set<Account> groupAccounts = this.getAccountByType(AccountType.TYPE_GROUP);
        for (Account groupAccount : groupAccounts) {
            groupUserNames.add(groupAccount.getUserName());
        }
    }

    /**
     * 加载群信息
     */
    public void loadGroupList() {
        log.info("加载群聊信息");

        // 群账号
        List<Map<String, String>> list = new ArrayList<>(groupUserNames.size());

        for (String groupUserName : groupUserNames) {
            Map<String, String> map = new HashMap<>(2);
            map.put("UserName", groupUserName);
            map.put("EncryChatRoomId", "");
            list.add(map);
        }

        String url = String.format("%s/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s",
                bot.session().getUrl(), System.currentTimeMillis() / 1000, bot.session().getPassTicket());

        // 加载群信息
        JsonResponse jsonResponse = this.client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Count", groupUserNames.size())
                .add("List", list)
        );

        this.groupList = WeChatUtils.fromJson(WeChatUtils.toJson(jsonResponse.toJsonObject().getAsJsonArray("ContactList")), new TypeToken<List<Account>>() {});
    }

    /**
     * 根据UserName查询Account
     *
     * @param id 用户UserName唯一标识
     * @return 返回找到的账户
     */
    @Override
    public Account getAccountById(String id) {
        return accountMap.get(id);
    }

    /**
     * 根据备注或昵称查找账户
     *
     * @param name
     * @return
     */
    @Override
    public Account getAccountByName(String name) {
        for (Account account : accountMap.values()) {
            if (name.equals(account.getRemarkName())) {
                return account;
            }
            if (name.equals(account.getNickName())) {
                return account;
            }
        }
        return null;
    }

    @Override
    public boolean revokeMsg(String msgId, String toUser) {
        String url = String.format("%s/webwxrevokemsg?pass_ticket=%s&r=%s",
                bot.session().getUrl(), bot.session().getPassTicket(), System.currentTimeMillis() / 1000);

        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("SvrMsgId", msgId)
                .add("ToUserName", toUser)
                .add("ClientMsgId", System.currentTimeMillis())
                .add("BaseRequest", bot.session().getBaseRequest())
        );
        return null != response && response.success();
    }

    @Override
    public boolean verify(Recommend recommend) {
        String url = String.format("%s/webwxverifyuser?r=%s&lang=zh_CN&pass_ticket=%s",
                bot.session().getUrl(), System.currentTimeMillis() / 1000, bot.session().getPassTicket());

        List<Map<String, Object>> verifyUserList = new ArrayList<>();
        Map<String, Object>       verifyUser     = new HashMap<>(2);
        verifyUser.put("Value", recommend.getUserName());
        verifyUser.put("VerifyUserTicket", recommend.getTicket());
        verifyUserList.add(verifyUser);

        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Opcode", 3)
                .add("VerifyUserListSize", 1)
                .add("VerifyUserList", verifyUserList)
                .add("VerifyContent", "")
                .add("SceneListCount", 1)
                .add("SceneList", Arrays.asList(33))
                .add("skey", bot.session().getSyncKeyStr())
        );
        return null != response && response.success();
    }

    @Override
    public boolean addFriend(String friendUserName, String msg) {
        String url = String.format("%s/webwxverifyuser?r=%s&lang=zh_CN&pass_ticket=%s",
                bot.session().getUrl(), System.currentTimeMillis() / 1000, bot.session().getPassTicket());

        List<Map<String, Object>> verifyUserList = new ArrayList<>();
        Map<String, Object>       verifyUser     = new HashMap<>(2);
        verifyUser.put("Value", friendUserName);
        verifyUser.put("VerifyUserTicket", "");
        verifyUserList.add(verifyUser);

        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Opcode", 2)
                .add("VerifyUserListSize", 1)
                .add("VerifyUserList", verifyUserList)
                .add("VerifyContent", msg)
                .add("SceneListCount", 1)
                .add("SceneList", Arrays.asList(33))
                .add("skey", bot.session().getSyncKeyStr())
        );
        return null != response && response.success();
    }

    @Override
    public boolean createChatRoom(String topic, List<String> members) {
        String                    url        = String.format("%s/webwxcreatechatroom?r=%s&lang=zh_CN", bot.session().getUrl());
        List<Map<String, String>> memberList = new ArrayList<>(members.size());
        for (String member : members) {
            Map<String, String> m = new HashMap<>(2);
            m.put("UserName", member);
            memberList.add(m);
        }

        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("MemberCount", members.size())
                .add("MemberList", memberList)
                .add("Topic", topic)
                .add("BaseRequest", bot.session().getBaseRequest())
        );
        return null != response && response.success();
    }

    @Override
    public boolean removeMemberByGroup(String member, String group) {
        String url = String.format("%s/webwxupdatechatroom?fun=delmember", bot.session().getUrl());
        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("DelMemberList", member)
                .add("ChatRoomName", group)
                .add("BaseRequest", bot.session().getBaseRequest())
        );
        return null != response && response.success();
    }

    @Override
    public boolean inviteJoinGroup(String member, String group) {
        String url = String.format("%s/webwxupdatechatroom?fun=addmember", bot.session().getUrl());
        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("AddMemberList", member)
                .add("ChatRoomName", group)
                .add("BaseRequest", bot.session().getBaseRequest())
        );
        return null != response && response.success();
    }

    @Override
    public boolean modifyGroupName(String oldTopic, String newTopic) {
        Account account = this.getAccountByName(oldTopic);
        if (null == account) {
            log.warn("找不到群: [{}] 更换群名失败", oldTopic);
            return false;
        }
        String url = String.format("%s/webwxupdatechatroom?fun=modtopic", bot.session().getUrl());
        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("NewTopic", newTopic)
                .add("ChatRoomName", account.getUserName())
                .add("BaseRequest", bot.session().getBaseRequest())
        );
        return null != response && response.success();
    }

    private String getUserRemarkName(String id) {
        String name = id.contains("@@") ? "未知群" : "陌生人";
        if (id.equals(this.bot.session().getUserName())) {
            return this.bot.session().getNickName();
        }
        Account account = accountMap.get(id);
        if (null == account) {
            return name;
        }
        String nickName = StringUtils.isNotEmpty(account.getRemarkName()) ? account.getRemarkName() : account.getNickName();
        return StringUtils.isNotEmpty(nickName) ? nickName : name;
    }

    private List<Account> getNameById(String id) {
        String url = String.format("%s/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s",
                bot.session().getUrl(), System.currentTimeMillis() / 1000, bot.session().getPassTicket());

        List<Map<String, String>> list = new ArrayList<>();
        Map<String, String>       map  = new HashMap<>();
        map.put("UserName", id);
        map.put("EncryChatRoomId", id);
        list.add(map);

        JsonResponse response = this.client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Count", bot.session().getBaseRequest())
                .add("List", list));

        return WeChatUtils.fromJson(WeChatUtils.toJson(response.toJsonObject().getAsJsonObject("")), new TypeToken<List<Account>>() {});
    }

    /**
     * 根据账号类型筛选
     *
     * @param accountType 账户类型
     * @return 返回筛选后的账户列表
     */
    public Set<Account> getAccountByType(AccountType accountType) {
        Set<Account> accountSet = new HashSet<>();
        for (Account account : accountMap.values()) {
            if (account.getAccountType().equals(accountType)) {
                accountSet.add(account);
            }
        }
        return accountSet;
    }

    /**
     * 同步最近联系人
     * <p>
     * 避免新建群聊无法同步
     *
     * @param contactList 联系人列表
     */
    public void syncRecentContact(List<Account> contactList) {
        if (null != contactList && contactList.size() > 0) {
            for (Account account : contactList) {
                accountMap.put(account.getUserName(), account);
            }
        }
    }

    /**
     * 处理新消息
     *
     * @param messages 要处理的消息列表
     */
    @Override
    public List<WeChatMessage> handleMsg(List<Message> messages) {
        if (null != messages && messages.size() > 0) {
            List<WeChatMessage> weChatMessages = new ArrayList<>(messages.size());
            boolean             hashNewMsg     = false;
            for (Message message : messages) {
                WeChatMessage weChatMessage = this.processMsg(message);
                if (null != weChatMessage) {
                    weChatMessages.add(weChatMessage);
                    hashNewMsg = true;
                }
            }
            if (hashNewMsg) {
                log.info("你有新的消息");
            }
            return weChatMessages;
        }
        return null;
    }

    private WeChatMessage processMsg(Message message) {
        Integer type    = message.getType();
        String  name    = this.getUserRemarkName(message.getFromUserName());
        String  msgId   = message.getId();
        String  content = message.getContent();

        // 不处理自己发的消息
        if (message.getFromUserName().equals(bot.session().getUserName())) {
            return null;
        }

        if (message.isGroup()) {
            // 如果本地缓存的群名列表没有当前群，则添加进去，下次更新使用
            if (message.getFromUserName().contains(GROUP_IDENTIFY) &&
                    !groupUserNames.contains(message.getFromUserName())) {
                this.groupUserNames.add(message.getFromUserName());
            }
            if (message.getToUserName().contains(GROUP_IDENTIFY) &&
                    !groupUserNames.contains(message.getToUserName())) {
                this.groupUserNames.add(message.getToUserName());
            }
            if (content.contains(GROUP_BR)) {
                content = content.substring(content.indexOf(GROUP_BR) + 6);
            }
        }

        content = WeChatUtils.formatMsg(content);

        WeChatMessage.WeChatMessageBuilder weChatMessageBuilder = WeChatMessage.builder()
                .raw(message)
                .id(message.getId())
                .fromUserName(message.getFromUserName())
                .toUserName(message.getToUserName())
                .mineUserName(bot.session().getUserName())
                .mineNickName(bot.session().getNickName())
                .msgType(message.msgType())
                .text(content);

        Account fromAccount = this.getAccountById(message.getFromUserName());
        if (null == fromAccount) {
            log.warn("消息类型: {}", message.msgType());
            log.warn("消息主体: {}", WeChatUtils.toPrettyJson(message));
        } else {
            weChatMessageBuilder.fromNickName(fromAccount.getNickName()).fromRemarkName(fromAccount.getRemarkName());
            if (log.isDebugEnabled()) {
                log.debug("收到消息JSON: {}", WeChatUtils.toJson(message));
            }
        }

        switch (message.msgType()) {
            case TEXT:
                // 被艾特的消息
                if (content.startsWith("@" + bot.session().getNickName())) {
                    content = content.substring(content.indexOf(" "));
                }
                // 位置消息
                if (content.contains(LOCATION_IDENTIFY)) {
                    int pos = content.indexOf(":");
                    content = content.substring(0, pos);
                    weChatMessageBuilder.isLocation(true).text(content);
                }
                return weChatMessageBuilder.text(content).build();
            // 聊天图片
            case IMAGE:
                String imgPath = this.downloadImg(msgId);
                return weChatMessageBuilder.imagePath(imgPath).build();
            // 语音
            case VOICE:
                String voicePath = this.downloadVoice(msgId);
                return weChatMessageBuilder.voicePath(voicePath).build();
            // 好友请求
            case ADD_FRIEND:
                return weChatMessageBuilder.text(message.getRecommend().getContent()).build();
            // 名片
            case PERSON_CARD:
                return weChatMessageBuilder.recommend(message.getRecommend()).build();
            // 视频
            case VIDEO:
                String videoPath = this.downloadVideo(msgId);
                return weChatMessageBuilder.videoPath(videoPath).build();
            // 动画表情
            case EMOTICONS:
                String imgUrl = this.searchContent("cdnurl", content);
                return weChatMessageBuilder.imagePath(imgUrl).build();
            // 分享
            case SHARE:
                String shareUrl = message.getUrl();
                return weChatMessageBuilder.text(shareUrl).build();
            // 联系人初始化
            case CONTACT_INIT:
                log.info("联系人初始化");
                return null;
            // 系统消息
            case SYSTEM:
                break;
            // 撤回消息
            case REVOKE_MSG:
                log.info("{} 撤回了一条消息: {}", name, content);
                return weChatMessageBuilder.build();
            default:
                log.info("该消息类型为: {}, 可能是表情，图片, 链接或红包: {}", type, WeChatUtils.toJson(message));
                break;
        }
        return weChatMessageBuilder.build();
    }

    private String searchContent(String key, String content) {
        String r = WeChatUtils.match(key + "\\s?=\\s?\"([^\"<]+)\"", content);
        if (StringUtils.isNotEmpty(r)) {
            return r;
        }
        r = WeChatUtils.match(String.format("<%s>([^<]+)</%s>", key, key), content);
        if (StringUtils.isNotEmpty(r)) {
            return r;
        }
        r = WeChatUtils.match(String.format("<%s><\\!\\[CDATA\\[(.*?)\\]\\]></%s>", key, key), content);
        if (StringUtils.isNotEmpty(r)) {
            return r;
        }
        return "";
    }

    /**
     * 下载图片到本地
     *
     * @param msgId 图片消息id
     * @return 返回图片本地路径
     */
    private String downloadImg(String msgId) {
        return this.downloadFile(
                new DownLoad(ApiURL.IMAGE, bot.session().getUrl(), msgId, bot.session().getSKey())
                        .msgId(msgId).saveByDay()
        );
    }

    /**
     * 下载表情到本地
     *
     * @param msgId icon消息id
     * @return 返回Icon本地路径
     */
    private String downloadIconImg(String msgId) {
        return this.downloadFile(
                new DownLoad(ApiURL.ICON, bot.session().getUrl(), msgId, bot.session().getSKey())
                        .msgId(msgId)
        );
    }

    /**
     * 下载头像到本地
     *
     * @param userName 用户名
     * @return 返回头像本地路径
     */
    private String downloadHeadImg(String userName) {
        return this.downloadFile(
                new DownLoad(ApiURL.HEAD_IMG, bot.session().getUrl(), userName, bot.session().getSKey())
                        .msgId(userName)
        );
    }

    /**
     * 下载视频到本地
     *
     * @param msgId 视频消息id
     * @return 返回视频本地路径
     */
    private String downloadVideo(String msgId) {
        return this.downloadFile(
                new DownLoad(ApiURL.VIDEO, bot.session().getUrl(), msgId, bot.session().getSKey())
                        .msgId(msgId).saveByDay()
        );
    }

    /**
     * 下载音频到本地
     *
     * @param msgId 音频消息id
     * @return 返回音频本地路径
     */
    private String downloadVoice(String msgId) {
        return this.downloadFile(
                new DownLoad(ApiURL.VOICE, bot.session().getUrl(), msgId, bot.session().getSKey())
                        .msgId(msgId).saveByDay()
        );
    }

    private String downloadFile(DownLoad downLoad) {
        String url = String.format(downLoad.getApiURL().getUrl(), downLoad.getParams());

        FileResponse response    = this.client.download(new FileRequest(url));
        InputStream  inputStream = response.getInputStream();

        String id  = downLoad.getFileName();
        String dir = downLoad.getDir(bot);
        return WeChatUtils.saveFileByDay(inputStream, dir, id, downLoad.isSaveByDay()).getPath();
    }

    /**
     * 上传附件
     *
     * @param toUser   发送给谁
     * @param filePath 文件路径
     * @return MediaResponse
     */
    @Override
    public MediaResponse uploadMedia(String toUser, String filePath) {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new WeChatException("文件[" + filePath + "]不存在");
        }
        log.info("开始上传文件: {}", filePath);
        long   size      = file.length();
        String mimeType  = WeChatUtils.getMimeType(filePath);
        String mediatype = "doc";
        if (mediatype.contains("image")) {
            mediatype = "pic";
        }
        if (mediatype.contains("audio")) {
            mediatype = "audio";
        }
        if (mediatype.contains("video")) {
            mediatype = "video";
        }
        String url     = String.format("%s/webwxuploadmedia?f=json", bot.session().getFileUrl());
        String mediaId = System.currentTimeMillis() / 1000 + StringUtils.random(6);

        Map<String, Object> uploadMediaRequest = new HashMap<>(10);
        uploadMediaRequest.put("UploadType", 2);
        uploadMediaRequest.put("BaseRequest", bot.session().getBaseRequest());
        uploadMediaRequest.put("ClientMediaId", mediaId);
        uploadMediaRequest.put("TotalLen", size);
        uploadMediaRequest.put("StartPos", 0);
        uploadMediaRequest.put("DataLen", size);
        uploadMediaRequest.put("MediaType", 4);
        uploadMediaRequest.put("FromUserName", bot.session().getUserName());
        uploadMediaRequest.put("ToUserName", toUser);
        uploadMediaRequest.put("FileMd5", MD5Checksum.getMD5Checksum(file.getPath()));

        String dataTicket = this.client.cookie("webwx_data_ticket");
        if (StringUtils.isEmpty(dataTicket)) {
            throw new WeChatException("缺少了附件Cookie");
        }

        ApiResponse response = this.client.send(new StringRequest(url).post().multipart()
                .fileName(file.getName())
                .add("id", "WU_FILE_0")
                .add("name", filePath)
                .add("type", mimeType)
                .add("lastModifieDate", new SimpleDateFormat("yyyy MM dd HH:mm:ss").format(new Date()))
                .add("size", String.valueOf(size))
                .add("mediatype", mediatype)
                .add("uploadmediarequest", WeChatUtils.toJson(uploadMediaRequest))
                .add("webwx_data_ticket", dataTicket)
                .add("pass_ticket", bot.session().getPassTicket())
                .add("filename", RequestBody.create(MediaType.parse(mimeType), file)));

        MediaResponse mediaResponse = response.parse(MediaResponse.class);
        if (!mediaResponse.success()) {
            log.warn("上传附件失败: {}", mediaResponse.getMsg());
        }
        log.info("文件上传成功: {}", filePath);
        return mediaResponse;
    }

    /**
     * 发送文件
     *
     * @param toUserName 发送给谁
     * @param filePath   文件路径
     */
    @Override
    public boolean sendImg(String toUserName, String filePath) {
        DateUtils.sendSleep();
        String mediaId = this.uploadMedia(toUserName, filePath).getMediaId();
        if (StringUtils.isEmpty(mediaId)) {
            log.warn("Media为空");
            return false;
        }

        String url = String.format("%s/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s",
                bot.session().getUrl(), bot.session().getPassTicket());

        String msgId = System.currentTimeMillis() / 1000 + StringUtils.random(6);

        Map<String, Object> msg = new HashMap<>();
        msg.put("Type", 3);
        msg.put("MediaId", mediaId);
        msg.put("FromUserName", bot.session().getUserName());
        msg.put("ToUserName", toUserName);
        msg.put("LocalID", msgId);
        msg.put("ClientMsgId", msgId);

        JsonResponse response = this.client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Msg", msg)
        );
        return null != response && response.success();
    }

    /**
     * 发送文本消息
     *
     * @param msg        文本消息
     * @param toUserName 发送给谁
     */
    @Override
    public boolean sendText(String toUserName, String msg) {
        DateUtils.sendSleep();
        String url   = String.format("%s/webwxsendmsg?pass_ticket=%s", bot.session().getUrl(), bot.session().getPassTicket());
        String msgId = System.currentTimeMillis() / 1000 + StringUtils.random(6);

        JsonResponse response = this.client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Msg", new SendMessage(1, msg, bot.session().getUserName(), toUserName, msgId, msgId))
        );
        return null != response && response.success();
    }

    @Override
    public boolean sendFile(String toUser, String filePath) {
        DateUtils.sendSleep();
        String        title         = new File(filePath).getName();
        MediaResponse mediaResponse = this.uploadMedia(toUser, filePath);
        if (null == mediaResponse) {
            log.warn("上传附件到微信出错");
            return false;
        }

        String url = String.format("%s/webwxsendappmsg?fun=async&f=json&pass_ticket=%s",
                bot.session().getUrl(), bot.session().getPassTicket());

        String fileSuffix = title.substring(title.lastIndexOf(".") + 1, title.length());

        String msgId = System.currentTimeMillis() + "";

        String content = String.format("<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''>" +
                        "<title>%s</title><des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>" +
                        "<appattach><totallen>%s</totallen><attachid>%s</attachid><fileext>%s</fileext></appattach><extinfo></extinfo></appmsg>",

                title, mediaResponse.getStartPos(), mediaResponse.getMediaId(), fileSuffix);

        Map<String, String> msgMap = new HashMap<>(6);
        msgMap.put("Type", "6");
        msgMap.put("Content", content);
        msgMap.put("FromUserName", bot.session().getUserName());
        msgMap.put("ToUserName", toUser);
        msgMap.put("LocalID", msgId);
        msgMap.put("ClientMsgId", msgId);

        JsonResponse response = client.send(new JsonRequest(url).post().jsonBody()
                .add("BaseRequest", bot.session().getBaseRequest())
                .add("Msg", msgMap)
                .add("Scene", 0)
        );
        return null != response && response.success();
    }

}


package io.github.biezhi.wechat.utils;

import java.io.*;
import java.security.MessageDigest;

public class MD5Checksum {

   public static byte[] createChecksum(String filename) throws Exception {
       InputStream fis =  new FileInputStream(filename);

       byte[] buffer = new byte[1024];
       MessageDigest complete = MessageDigest.getInstance("MD5");
       int numRead;

       do {
           numRead = fis.read(buffer);
           if (numRead > 0) {
               complete.update(buffer, 0, numRead);
           }
       } while (numRead != -1);

       fis.close();
       return complete.digest();
   }

   // see this How-to for a faster way to convert
   // a byte array to a HEX string
   public static String getMD5Checksum(String filename) {
       try {
           byte[] b = createChecksum(filename);
           String result = "";

           for (int i=0; i < b.length; i++) {
               result += Integer.toString( ( b[i] & 0xff ) + 0x100, 16).substring( 1 );
           }
           return result;
       } catch (Exception e){
           return "";
       }
   }

   public static void main(String args[]) {
       try {
           System.out.println(getMD5Checksum("apache-tomcat-5.5.17.exe"));
           // output :
           //  0bb2827c5eacf570b6064e24e0e6653b
           // ref :
           //  http://www.apache.org/dist/
           //          tomcat/tomcat-5/v5.5.17/bin
           //              /apache-tomcat-5.5.17.exe.MD5
           //  0bb2827c5eacf570b6064e24e0e6653b *apache-tomcat-5.5.17.exe
       }
       catch (Exception e) {
           e.printStackTrace();
       }
   }
}


package io.github.biezhi.wechat.api.client;

import io.github.biezhi.wechat.api.constant.Constant;
import io.github.biezhi.wechat.api.request.ApiRequest;
import io.github.biezhi.wechat.api.response.ApiResponse;
import io.github.biezhi.wechat.api.response.FileResponse;
import io.github.biezhi.wechat.exception.WeChatException;
import io.github.biezhi.wechat.utils.WeChatUtils;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;

import java.io.File;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * 微信机器人 HTTP 发送端
 *
 * @author biezhi
 * @date 2018/1/18
 */
@Slf4j
public class BotClient {

    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    private static Map<String, List<Cookie>> cookieStore = new ConcurrentHashMap<>();

    private OkHttpClient client;
    private OkHttpClient clientWithTimeout;

    public BotClient(OkHttpClient client) {
        this.client = client;
        System.setProperty("https.protocols", "TLSv1");
        System.setProperty("jsse.enableSNIExtension", "false");
    }

    /**
     * 重新恢复Cookie
     *
     * @param cookieStore
     */
    public static void recoverCookie(Map<String, List<Cookie>> cookieStore) {
        BotClient.cookieStore.clear();
        BotClient.cookieStore = cookieStore;
    }

    public static Map<String, List<Cookie>> cookieStore() {
        return cookieStore;
    }

    public <T extends ApiRequest, R extends ApiResponse> void send(final T request, final Callback<T, R> callback) {
        OkHttpClient client = getOkHttpClient(request);
        client.newCall(createRequest(request)).enqueue(new okhttp3.Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                try {
                    String body = response.body().string();
                    if (log.isDebugEnabled()) {
                        log.debug("Response:\r\n{}", body);
                    }
                    if (ApiResponse.class.equals(request.getResponseType())) {
                        callback.onResponse(request, (R) new ApiResponse(body));
                    } else {
                        R result = WeChatUtils.fromJson(body, request.getResponseType());
                        result.setRawBody(body);
                        callback.onResponse(request, result);
                    }
                } catch (Exception e) {
                    IOException ioEx = e instanceof IOException ? (IOException) e : new IOException(e);
                    callback.onFailure(request, ioEx);
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                callback.onFailure(request, e);
            }
        });
    }

    public <T extends ApiRequest, R extends ApiResponse> R send(final ApiRequest<T, R> request) {
        try {
            OkHttpClient client        = getOkHttpClient(request);
            Request      okHttpRequest = createRequest(request);
            Response     response      = client.newCall(okHttpRequest).execute();
            String       body          = response.body().string();

            if (log.isDebugEnabled()) {
                log.debug("Response :\r\n{}", body);
            }

            // 获取头部的Cookie,注意：可以通过Cooke.parseAll()来获取
            List<Cookie> cookies = Cookie.parseAll(okHttpRequest.url(), response.headers());
            // 防止header没有Cookie的情况
            if (cookies != null && cookies.size() > 0) {
                cookieStore.put(okHttpRequest.url().host(), cookies);
                if (!"webpush.web.wechat.com".equals(okHttpRequest.url().host())) {
                    cookieStore.put("webpush.web.wechat.com", cookies);
                }
                if (!"webpush.wx2.qq.com".equals(okHttpRequest.url().host())) {
                    cookieStore.put("webpush.wx2.qq.com", cookies);
                }
                if (!"wx2.qq.com".equals(okHttpRequest.url().host())) {
                    cookieStore.put("wx2.qq.com", cookies);
                }
                if (!"file.web.wechat.com".equals(okHttpRequest.url().host())) {
                    cookieStore.put("file.web.wechat.com", cookies);
                }
                if (!"web.wechat.com".equals(okHttpRequest.url().host())) {
                    cookieStore.put("web.wechat.com", cookies);
                }
            }

            if (ApiResponse.class.equals(request.getResponseType())) {
                return (R) new ApiResponse(body);
            }
            R result = WeChatUtils.fromJson(body, request.getResponseType());
            result.setRawBody(body);
            return result;
        } catch (IOException e) {
            throw new WeChatException(e);
        }
    }

    public <T extends ApiRequest, R extends ApiResponse> R download(final ApiRequest<T, R> request) {
        try {
            OkHttpClient client   = getOkHttpClient(request);
            Response     response = client.newCall(createRequest(request)).execute();
            return (R) new FileResponse(response.body().byteStream());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private OkHttpClient getOkHttpClient(ApiRequest request) {
        OkHttpClient client = timeout(request);
        if (request.isNoRedirect()) {
            return client.newBuilder().followRedirects(false).followSslRedirects(false).build();
        }
        return cookie(client);
    }

    /**
     * 设置超时
     *
     * @param request
     * @return
     */
    private OkHttpClient timeout(ApiRequest request) {
        int timeoutMillis = request.getTimeout() * 1000;
        if (client.readTimeoutMillis() == 0 || client.readTimeoutMillis() > timeoutMillis) {
            return client;
        }
        if (null != clientWithTimeout && clientWithTimeout.readTimeoutMillis() > timeoutMillis) {
            return clientWithTimeout;
        }
        clientWithTimeout = client.newBuilder().readTimeout(timeoutMillis + 1000, TimeUnit.MILLISECONDS).build();
        return clientWithTimeout;
    }

    private OkHttpClient cookie(OkHttpClient client) {
        return client.newBuilder().cookieJar(new CookieJar() {
            @Override
            public void saveFromResponse(HttpUrl httpUrl, List<Cookie> cookies) {
            }

            @Override
            public List<Cookie> loadForRequest(HttpUrl httpUrl) {
                List<Cookie> cookies = cookieStore.get(httpUrl.host());
                return cookies != null ? cookies : new ArrayList<Cookie>();
            }
        }).build();
    }

    public List<Cookie> cookies() {
        List<Cookie>             cookies = new ArrayList<Cookie>();
        Collection<List<Cookie>> values  = cookieStore.values();
        for (List<Cookie> value : values) {
            cookies.addAll(value);
        }
        return cookies;
    }

    public String cookie(String name) {
        for (Cookie cookie : cookies()) {
            if (cookie.name().equalsIgnoreCase(name)) {
                return cookie.value();
            }
        }
        return null;
    }

    private Request createRequest(ApiRequest request) {
        Request.Builder builder = new Request.Builder();
        if (Constant.GET.equalsIgnoreCase(request.getMethod())) {
            builder.get();
            if (null != request.getParameters() && request.getParameters().size() > 0) {
                Set<String>   keys = request.getParameters().keySet();
                StringBuilder sbuf = new StringBuilder(request.getUrl());
                if (request.getUrl().contains("=")) {
                    sbuf.append("&");
                } else {
                    sbuf.append("?");
                }
                for (String key : keys) {
                    sbuf.append(key).append('=').append(request.getParameters().get(key)).append('&');
                }
                request.url(sbuf.substring(0, sbuf.length() - 1));
            }
        } else {
            builder.method(request.getMethod(), createRequestBody(request));
        }
        builder.url(request.getUrl());
        if (log.isDebugEnabled()) {
            log.debug("Request : {}", request.getUrl());
        }
        if (null != request.getHeaders()) {
            builder.headers(request.getHeaders());
        }
        return builder.build();
    }

    private RequestBody createRequestBody(ApiRequest<?, ?> request) {
        if (request.isMultipart()) {
            MediaType             contentType = MediaType.parse(request.getContentType());
            MultipartBody.Builder builder     = new MultipartBody.Builder().setType(MultipartBody.FORM);

            for (Map.Entry<String, Object> parameter : request.getParameters().entrySet()) {
                String name  = parameter.getKey();
                Object value = parameter.getValue();
                if (value instanceof byte[]) {
                    builder.addFormDataPart(name, request.getFileName(), RequestBody.create(contentType, (byte[]) value));
                } else if (value instanceof File) {
                    builder.addFormDataPart(name, request.getFileName(), RequestBody.create(contentType, (File) value));
                } else if (value instanceof RequestBody) {
                    builder.addFormDataPart(name, request.getFileName(), (RequestBody) value);
                } else {
                    builder.addFormDataPart(name, String.valueOf(value));
                }
            }
            return builder.build();
        } else {
            if (request.isJsonBody()) {
                String json = WeChatUtils.toJson(request.getParameters());
                if (log.isDebugEnabled()) {
                    log.debug("Request Body:\r\n{}", json);
                }
                return RequestBody.create(JSON, json);
            } else {
                FormBody.Builder builder = new FormBody.Builder();
                for (Map.Entry<String, Object> parameter : request.getParameters().entrySet()) {
                    builder.add(parameter.getKey(), String.valueOf(parameter.getValue()));
                }
                return builder.build();
            }

        }
    }

}


package io.github.biezhi.wechat.utils;

import com.google.zxing.*;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.extern.slf4j.Slf4j;

import javax.imageio.ImageIO;
import java.awt.*;
import java.io.File;
import java.io.FileInputStream;
import java.util.EnumMap;
import java.util.Map;

/**
 * 二维码工具类
 *
 * @author biezhi
 * @date 2018/1/18
 */
@Slf4j
public class QRCodeUtils {

    /**
     * 显示二维码，Linux会显示在终端
     *
     * @param qrCode
     * @param terminal
     */
    public static void showQrCode(File qrCode, boolean terminal) throws WriterException {
        if (!terminal) {
            String os = System.getProperty("os.name").toLowerCase();
            try {
                if (os.contains("mac") || os.contains("win")) {
                    Desktop.getDesktop().open(qrCode);
                    return;
                }
            } catch (Exception e) {
                log.warn("在 {} 下打开文件 {} 失败", os, qrCode.getPath(), e);
            }
        }
        Map<EncodeHintType, Object> hintMap = new EnumMap<EncodeHintType, Object>(EncodeHintType.class);
        hintMap.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        // Now with zxing version 3.2.1 you could change border size (white border size to just 1)
        // default = 4
        hintMap.put(EncodeHintType.MARGIN, 1);
        hintMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);

        String       qrContent    = QRCodeUtils.readQRCode(qrCode, hintMap);
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix    bitMatrix;
        bitMatrix = qrCodeWriter.encode(qrContent, BarcodeFormat.QR_CODE, 10, 10, hintMap);
        System.out.println(toAscii(bitMatrix));
    }

    /**
     * 将二维码输出为 ASCII
     *
     * @param bitMatrix
     * @return
     */
    private static String toAscii(BitMatrix bitMatrix) {
        StringBuilder sb = new StringBuilder();
        for (int rows = 0; rows < bitMatrix.getHeight(); rows++) {
            for (int cols = 0; cols < bitMatrix.getWidth(); cols++) {
                boolean x = bitMatrix.get(rows, cols);
                if (!x) {
                    // white
                    sb.append("\033[47m  \033[0m");
                } else {
                    sb.append("\033[30m  \033[0;39m");
                }
            }
            sb.append("\n");
        }
        return sb.toString();
    }

    /**
     * 读取二维码信息
     *
     * @param filePath 文件路径
     * @param hintMap  hintMap
     * @return 二维码内容
     */
    private static String readQRCode(File filePath, Map hintMap) {
        try {
            BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(
                    new BufferedImageLuminanceSource(ImageIO.read(new FileInputStream(filePath)))));

            Result qrCodeResult = new MultiFormatReader().decode(binaryBitmap, hintMap);
            return qrCodeResult.getText();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}


package io.github.biezhi.wechat.utils;

import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;

import javax.net.ssl.*;

/**
 * OkHttp SSL 工具类
 */
@Slf4j
public class OkHttpUtils {

    /**
     * Setting testMode configuration. If set as testMode, the connection will skip certification check
     *
     * @param builder
     * @return
     */
    public static OkHttpClient.Builder configureToIgnoreCertificate(OkHttpClient.Builder builder) {
        log.warn("Ignore Ssl Certificate");
        try {
            /*Create a trust manager that does not validate certificate chains*/
            final TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                        }

                        @Override
                        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                        }

                        @Override
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return new java.security.cert.X509Certificate[]{};
                        }
                    }
            };

            /*Install the all-trusting trust manager*/
            final SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
            /*Create an ssl socket factory with our all-trusting manager*/
            final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

            builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
            builder.hostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            });
        } catch (Exception e) {
            log.warn("Exception while configuring IgnoreSslCertificate" + e, e);
        }
        return builder;
    }

}


/*
   Copyright 2014 Immutables Authors and Contributors

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package org.immutables.generator;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.eclipse.jdt.internal.compiler.apt.model.ElementImpl;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;

/**
 * Utility that abstracts away hacks to retrieve elements in source order. Currently, Javac returns
 * elements in proper source order, but EJC returns elements in alphabetical order.
 *

 *
Bug 300408 -
 * TypeElement.getEnclosedElements does not respect source order
 *
JDK-6884227 : Clarify ordering
 * requirements of javax.lang.model.TypeElement.getEnclosedElements
 *

 *

 * Based on a workaround idea provided by Christian Humer
 */
public final class SourceOrdering {
  private SourceOrdering() {}

  private interface OrderingProvider {
    Ordering<Element> enclosedBy(Element element);
  }

  private static final OrderingProvider DEFAULT_PROVIDER = new OrderingProvider() {
    // it's safe to cast ordering because it handles elements without regards of actual types.
    @SuppressWarnings("unchecked")
    @Override
    public Ordering<Element> enclosedBy(Element element) {
      return (Ordering<Element>) Ordering.explicit(element.getEnclosedElements());
    }
  };

  private static final OrderingProvider PROVIDER = createProvider();

  // it's safe to cast immutable list of <? extends Element> to a list of <Element>
  @SuppressWarnings("unchecked")
  public static ImmutableList<Element> getEnclosedElements(Element element) {
    return (ImmutableList<Element>) enclosedBy(element).immutableSortedCopy(element.getEnclosedElements());
  }

  public static Ordering<Element> enclosedBy(Element element) {
    return PROVIDER.enclosedBy(element);
  }

  private static OrderingProvider createProvider() {
    if (Compiler.ECJ.isPresent()) {
      return new EclipseCompilerOrderingProvider();
    }
    return DEFAULT_PROVIDER;
  }

  /**
   * This inner static class will fail to load if Eclipse compliler classes will not be in
   * classpath.
   * If annotation processor is executed by Javac compiler in presence of ECJ classes, then
   * instanceof checks will fail with fallback to defaults (Javac).
   */
  private static class EclipseCompilerOrderingProvider
      implements OrderingProvider, Function<Element, Object> {

    @Override
    public Object apply(Element input) {
      return ((ElementImpl) input)._binding;
    }

    @Override
    public Ordering<Element> enclosedBy(Element element) {
      if (element instanceof ElementImpl
          && Iterables.all(element.getEnclosedElements(), Predicates.instanceOf(ElementImpl.class))) {

        ElementImpl implementation = (ElementImpl) element;
        if (implementation._binding instanceof SourceTypeBinding) {
          SourceTypeBinding sourceBinding = (SourceTypeBinding) implementation._binding;

          return Ordering.natural().onResultOf(
              Functions.compose(bindingsToSourceOrder(sourceBinding), this));
        }
      }

      return DEFAULT_PROVIDER.enclosedBy(element);
    }

    private Function<Object, Integer> bindingsToSourceOrder(SourceTypeBinding sourceBinding) {
      IdentityHashMap<Object, Integer> bindings = Maps.newIdentityHashMap();

      if (sourceBinding.scope.referenceContext.methods != null) {
        for (AbstractMethodDeclaration declaration : sourceBinding.scope.referenceContext.methods) {
          bindings.put(declaration.binding, declaration.declarationSourceStart);
        }
      }
      if (sourceBinding.scope.referenceContext.fields != null) {
        for (FieldDeclaration declaration : sourceBinding.scope.referenceContext.fields) {
          bindings.put(declaration.binding, declaration.declarationSourceStart);
        }
      }
      if (sourceBinding.scope.referenceContext.memberTypes != null) {
        for (TypeDeclaration declaration : sourceBinding.scope.referenceContext.memberTypes) {
          bindings.put(declaration.binding, declaration.declarationSourceStart);
        }
      }
      return Functions.forMap(bindings);
    }
  }

  public interface AccessorProvider {
    ImmutableListMultimap<String, TypeElement> accessorMapping();

    ImmutableList<ExecutableElement> get();
  }

  /**
   * While we have {@link SourceOrdering}, there's still a problem: We have inheritance hierarchy
   * and
   * we want to have all defined or inherited accessors returned as members of target type, like
   * {@link Elements#getAllMembers(TypeElement)}, but we need to have them properly and stably
   * sorted.
   * This implementation doesn't try to correctly resolve order for accessors inherited from
   * different supertypes(interfaces), just something that stable and reasonable wrt source ordering
   * without handling complex cases.
   * @param elements the elements utility
   * @param types the types utility
   * @param originatingType the type to traverse
   * @return provider of all accessors in source order and mapping
   */
  public static AccessorProvider getAllAccessorsProvider(
      final Elements elements,
      final Types types,
      final TypeElement originatingType) {

    class CollectedOrdering extends Ordering<Element> {
      class Intratype {
        final String inType;
        final Ordering<String> ordering;
        final int rank;

        Intratype(String inType, int rank, List<String> accessors) {
          this.inType = inType;
          this.rank = rank;
          this.ordering = Ordering.explicit(accessors);
        }

        @Override
        public String toString() {
          return "(<=> " + inType + ", " + rank + ", " + ordering + ")";
        }
      }

      final Map<String, Intratype> accessorOrderings = new LinkedHashMap<>();
      final Set<TypeElement> linearizedTypes = new LinkedHashSet<>();
      final ArrayListMultimap<String, TypeElement> accessorMapping = ArrayListMultimap.create();

      CollectedOrdering() {
        traverse(originatingType);
        collectAccessors();
      }

      void traverse(@Nullable TypeElement element) {
        if (element == null || isJavaLangObject(element)) {
          return;
        }
        for (TypeMirror implementedInterface : element.getInterfaces()) {
          traverse(toElement(implementedInterface));
        }
        if (element.getKind().isClass()) {
          // collectEnclosing(element);
          traverse(toElement(element.getSuperclass()));
        }
        // we add this after so we start with the deepest
        linearizedTypes.add(element);
      }

      @Nullable
      TypeElement toElement(TypeMirror type) {
        if (type.getKind() == TypeKind.DECLARED) {
          return (TypeElement) ((DeclaredType) type).asElement();
        }
        return null;
      }

      void collectAccessors() {
        int i = 0;
        for (TypeElement type : linearizedTypes) {
          List<String> accessorsInType =
              FluentIterable.from(SourceOrdering.getEnclosedElements(type))
                  .filter(IsParameterlessNonstaticNonobject.PREDICATE)
                  .transform(ToSimpleName.FUNCTION)
                  .toList();

          String typeTag = type.getSimpleName().toString();
          Intratype intratype = new Intratype(typeTag, i++, accessorsInType);

          for (String name : accessorsInType) {
            // we override accessors by the ones redeclared in later types
            accessorMapping.put(name, type);
            accessorOrderings.put(name, intratype);
          }
        }
      }

      @Override
      public int compare(Element left, Element right) {
        String leftKey = ToSimpleName.FUNCTION.apply(left);
        String rightKey = ToSimpleName.FUNCTION.apply(right);
        Intratype leftIntratype = accessorOrderings.get(leftKey);
        Intratype rightIntratype = accessorOrderings.get(rightKey);
        return leftIntratype == rightIntratype
            ? leftIntratype.ordering.compare(leftKey, rightKey)
            : Integer.compare(leftIntratype.rank, rightIntratype.rank);
      }
    }

    final CollectedOrdering ordering = new CollectedOrdering();

    final ImmutableList<ExecutableElement> sortedList =
        ordering.immutableSortedCopy(
            disambiguateMethods(ElementFilter.methodsIn(
                elements.getAllMembers(originatingType))));

    return new AccessorProvider() {
      ImmutableListMultimap<String, TypeElement> accessorMapping =
          ImmutableListMultimap.copyOf(ordering.accessorMapping);

      @Override
      public ImmutableListMultimap<String, TypeElement> accessorMapping() {
        return accessorMapping;
      }

      @Override
      public ImmutableList<ExecutableElement> get() {
        return sortedList;
      }
    };
  }

  private static List<ExecutableElement> disambiguateMethods(
      Iterable<? extends ExecutableElement> methods) {

    Multimap<String, ExecutableElement> methodsAlternatives = HashMultimap.create();
    for (ExecutableElement m : methods) {
      if (IsParameterlessNonstaticNonobject.PREDICATE.apply(m)) {
        methodsAlternatives.put(ToSimpleName.FUNCTION.apply(m), m);
      }
    }

    List<ExecutableElement> resolvedMethods = Lists.newArrayList();

    entries: for (Entry<String, Collection<ExecutableElement>> e : methodsAlternatives.asMap().entrySet()) {
      Collection<ExecutableElement> values = e.getValue();
      if (values.size() == 1) {
        resolvedMethods.addAll(values);
      } else {
        // Preferably take the one coming from a class rather than interface
        for (ExecutableElement v : values) {
          if (v.getEnclosingElement().getKind().isClass()) {
            resolvedMethods.add(v);
            continue entries;
          }
        }
        // grab just the first among interface names
        for (ExecutableElement v : values) {
          resolvedMethods.add(v);
          continue entries;
        }
        // Expect that compilation error in user code
        // will happen if methods are not compatible, they have to
        // be resolved either way.
      }
    }

    return resolvedMethods;
  }

  private enum ToSimpleName implements Function<Element, String> {
    FUNCTION;
    @Override
    public String apply(Element input) {
      return input.getSimpleName().toString();
    }
  }

  private enum IsParameterlessNonstaticNonobject implements Predicate<Element> {
    PREDICATE;
    @Override
    public boolean apply(Element input) {
      if (input.getKind() != ElementKind.METHOD) {
        return false;
      }
      if (isJavaLangObject((TypeElement) input.getEnclosingElement())) {
        return false;
      }
      ExecutableElement element = (ExecutableElement) input;
      boolean parameterless = element.getParameters().isEmpty();
      boolean nonstatic = !element.getModifiers().contains(Modifier.STATIC);
      return parameterless && nonstatic;
    }
  }

  static boolean isJavaLangObject(TypeElement element) {
    return element.getQualifiedName().contentEquals(Object.class.getName());
  }
}


/*
   Copyright 2015 Immutables Authors and Contributors

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package org.immutables.gson.adapter;

import com.fasterxml.jackson.databind.util.TokenBuffer;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.bind.JsonTreeReader;
import com.google.gson.internal.bind.TypeAdapters;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import org.immutables.gson.stream.JsonParserReader;

/**
 * Composite type adapter used for polymorphic serialization.
 * It just buffers input and try each of delegating type adapters until succeeded. While there may
 * be legit concern about performance, it is most flexible way to handle subtypes by structure and
 * not by discriminator fields of some sort.
 * If failed with all type adapters exception will be thrown, attaching suppressed exceptions for
 * individual delegate adapters.
 * @param <T> supertype to adapt
 */
public final class ExpectedSubtypesAdapter<T> extends TypeAdapter<T> {
  private final TypeToken<T> type;
  private final Gson gson;
  private final List<TypeAdapter<? extends T>> adapters;
  private final TypeToken<? extends T>[] subtypes;

  /**
   * Creates adapter from {@link Gson} and type tokens.
   * @param <T> the generic type
   * @param gson Gson instance
   * @param type supertype token
   * @param subtypes subtype tokens
   * @return subtype adapter
   */
  @SafeVarargs
  public static <T> ExpectedSubtypesAdapter<T> create(
      Gson gson,
      TypeToken<T> type,
      TypeToken<? extends T>... subtypes) {
    return new ExpectedSubtypesAdapter<>(gson, type, subtypes);
  }

  @SafeVarargs
  public static <T> ExpectedSubtypesAdapter<T> create(
      Gson gson,
      Class<T> type,
      TypeToken<? extends T>... subtypes) {
    return create(gson, TypeToken.get(type), subtypes);
  }

  private ExpectedSubtypesAdapter(Gson gson, TypeToken<T> type, TypeToken<? extends T>[] subtypes) {
    if (subtypes.length < 1) {
      throw new IllegalArgumentException("At least one subtype should be specified");
    }
    if (gson == null) {
      throw new NullPointerException("supplied Gson is null");
    }
    if (type == null) {
      throw new NullPointerException("supplied type Gson is null");
    }
    this.gson = gson;
    this.type = type;
    this.subtypes = subtypes.clone();
    this.adapters = lookupAdapters();
  }

  private List<TypeAdapter<? extends T>> lookupAdapters() {
    List<TypeAdapter<? extends T>> adapters = new ArrayList<>(subtypes.length);
    for (TypeToken<? extends T> subtype : subtypes) {
      adapters.add(gson.getAdapter(subtype));
    }
    return adapters;
  }

  public TypeToken<T> getType() {
    return type;
  }

  @Override
  public void write(JsonWriter out, T value) throws IOException {
    if (value == null) {
      out.nullValue();
      return;
    }
    for (int i = 0; i < subtypes.length; i++) {
      TypeToken<? extends T> subtype = subtypes[i];
      if (subtype.getRawType().isInstance(value)) {
        // safe unchecked, type is checked at runtime
        @SuppressWarnings("unchecked") TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) adapters.get(i);
        typeAdapter.write(out, value);
        return;
      }
    }
    gson.toJson(value, value.getClass(), out);
  }

  // safe unchecked. expected that supplied adapters are producing correct types
  // enforced by constructor parameters.
  @SuppressWarnings("unchecked")
  @Override
  public T read(JsonReader in) throws IOException {
    List<Exception> exceptions = new ArrayList<>(subtypes.length);
    ReaderSupplier readerSupplier = readForSupplier(in);
    for (TypeAdapter<?> typeAdapter : adapters) {
      try {
        return (T) typeAdapter.read(readerSupplier.create());
      } catch (Exception ex) {
        exceptions.add(ex);
      }
    }
    JsonParseException failure = new JsonParseException(
        String.format(
            "Cannot parse %s with following subtypes: %s",
            type,
            Arrays.toString(subtypes)));
    for (Exception exception : exceptions) {
      failure.addSuppressed(exception);
    }
    throw failure;
  }

  private ReaderSupplier readForSupplier(JsonReader in) throws IOException {
    // check Callable marker for Jackson implementation.
    if (in instanceof Callable<?>) {
      return new JsonParserReaderSupplier(in);
    }
    return new JsonReaderSupplier(in);
  }

  private interface ReaderSupplier {
    JsonReader create();
  }

  /**
   * Default implementation for {@link ReaderSupplier} uses {@link JsonElement} as buffer,
   * while creating {@link JsonReader} to parse using each type adapter.
   */
  private static class JsonReaderSupplier implements ReaderSupplier {
    private final JsonElement element;

    JsonReaderSupplier(JsonReader in) throws IOException {
      this.element = TypeAdapters.JSON_ELEMENT.read(in);
    }

    @Override
    public JsonReader create() {
      return new JsonTreeReader(element);
    }
  }

  /**
   * Jackson buffer copy. Use of Jackson's own mechanisms is important to preserve custom elements
   * such as special embedded objects in BSON or other data formats. Jackson classes should not leak
   * outside of this class, so when there's no Jackson available in classpath, it will still work
   * with default {@link JsonReaderSupplier}.
   */
  private static class JsonParserReaderSupplier implements ReaderSupplier {
    private final TokenBuffer buffer;

    @SuppressWarnings("resource")
    JsonParserReaderSupplier(JsonReader in) throws IOException {
      buffer = ((JsonParserReader) in).nextTokenBuffer();
    }

    @Override
    public JsonReader create() {
      return new JsonParserReader(buffer.asParser());
    }
  }
}